JSMQL

Complex MongoDB queries in simple JavaScript

JSMQL is plain JavaScript syntax that compiles to MongoDB's query language: everyday JavaScript in, native MQL out.

Try the playground $ npm install @koresar/jsmql

Filter Compiled in the browser, right now

JSMQL
$.items.sumBy((i) => i.qty * i.price) > 100
MongoDB MQL
compiling…

One expression. Seventy lines of MQL.

Expression Full address from six optional fields

JSMQL
[$.building && $.building + ",", $.streetNo, $.street, $.suburb, $.state, $.country, $.postcode]
  .filter(Boolean)
  .join(" ")
MongoDB MQL
compiling…

SQL vs JSMQL

SQLJSMQL
Build a string
PostgreSQL
'Order #' || n || ': ' || total || ' AUD'
MySQL
CONCAT('Order #', n, ': ', total, ' AUD')
SQL Server
'Order #' + CAST(n AS varchar) + ': '
          + CAST(total AS varchar) + ' AUD'
`Order #${$.n}: ${$.total} AUD`
MQL in the playground →
Start of the month
PostgreSQL
DATE_TRUNC('month', created_at)
MySQL
DATE_FORMAT(created_at, '%Y-%m-01')
SQL Server
DATETRUNC(month, created_at)
$.createdAt.startOf("month")
MQL in the playground →
Join the tags into one string
COALESCE((SELECT string_agg(t.tag, ', ')
   FROM order_tags t WHERE t.order_id = o.id), '')
$.tags.join(", ")
MQL in the playground →
Filter after the group
SELECT customer_id, SUM(total) AS spent FROM orders
GROUP BY customer_id HAVING SUM(total) > 1000
$$.$group({ _id: $.customerId, spent: $sum($.total) })
  .filter((c) => c.spent > 1000);
MQL in the playground →
Latest order per customer
SELECT * FROM (
  SELECT *, ROW_NUMBER() OVER
    (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
  FROM orders) t WHERE rn = 1
$$.orderBy({ createdAt: -1 }).uniqBy("customerId");
MQL in the playground →
Count related rows
(SELECT COUNT(*) FROM payments p
   WHERE p.order_id = o.id AND p.status = 'failed')
$.failed = $$$.payments.filter(
  (p) => p.orderId === $._id && p.status === "failed",
).length;
MQL in the playground →

How it compiles

Every MQL document on this page is compiled in the browser.

Pipeline Stages are statements

JSMQL
$match($.age >= 18 && $.region === "AU");
$group({ _id: $.shopId, total: $sum($.amount) });
$sort({ total: -1 });
MongoDB MQL
compiling…

Expression Array work reads like JavaScript

JSMQL
$.items.map("price").sum()
MongoDB MQL
compiling…

Update Assignment, += and delete

JSMQL
$.score += 1;
delete $.tempToken;
$.status = "done";
MongoDB MQL
compiling…

Pipeline Joins without hand-writing $lookup

JSMQL
$.customer = $$$.customers.find({ _id: $.customerId });
$.payments = $$$.payments.filter({ orderId: $._id, status: "paid" });
MongoDB MQL
compiling…

Why JSMQL

MQL is an AST

A developer thinks $.qty * $.price + $.shipping. MongoDB needs { $add: [{ $multiply: ["$qty", "$price"] }, "$shipping"] }. That is the abstract syntax tree of the expression. Writing MQL by hand is writing an AST by hand.

LLM-generated MQL still needs to be reviewed

Code generated by an LLM still has to be reviewed by a person. The smaller the diff, the faster the review: five lines of JavaScript are read in seconds; 250 lines of nested $reduce are not. LLMs like smaller diffs too — less to generate, less to get wrong.

Existing MQL keeps working

Any MQL pasted into JSMQL comes out unchanged, so existing MQL code will work out of the box.

Every operator is a function call

Any MongoDB operator can be written as a function: $op(args) becomes { $op: args }. $dateTrunc({ date: $.createdAt, unit: "week" }) compiles to { $dateTrunc: { date: "$createdAt", unit: "week" } }, and $setUnion($.tags, $.extra) to { $setUnion: ["$tags", "$extra"] }. This also covers operators that do not exist yet.

Plain JavaScript syntax

Your IDE syntax highlighting, formatting and refactoring will work without a plugin. Autocomplete for every operator ships in the package.

Indexes keep working

JSMQL makes sure that collection indexes are used as much as possible.

Errors suggest a fix

Every error message tries to suggest a fix for the syntax error it reports.

Never invalid MQL

JSMQL makes sure it never generates invalid MQL — unless you write $typo yourself.

Not server-side JavaScript

MongoDB 8.0 deprecated $function, $accumulator and $where: JavaScript running inside the server, per document, without indexes. Rightly. JSMQL compiles ahead of time, on the application side; the server only ever sees ordinary MQL.

Computations on the server

A typical pattern: fetch all the documents, then filter, group and sum them on the client side. JSMQL helps you move that computation to the database server.

No need to learn a new programming language

Two in three developers write JavaScript, and lodash is downloaded tens of millions of times a week. .filter, .groupBy, .sumBy are already known words.

SQL over MongoDB keeps failing

Since 2015 MongoDB has offered analysts SQL three times: a BI connector built on PostgreSQL, then its Atlas successor now deprecated, and a read-only SQL-92 interface. Each assumed the people who need data speak SQL. JSMQL assumes they speak JavaScript.

Simple way to reference sibling collections

$ is the document, $$ the collection being queried, $$$ the database, $$$$ the current MongoDB server.

Parse once, run many times

On a hot path the same JSMQL should not be compiled on every request. jsmql.compile((params, { $ }) => …) parses it once, at startup, and returns a function; each call only binds the parameter values.

Mongoose plugin

Model methods accept JSMQL wherever they take a filter, an update or a pipeline; plain MQL keeps working untouched. UserModel.find(`$.email.endsWith("@gmail.com")`), or OrderModel.aggregate(() => $$.groupBy("sku")).

Command line

echo '$.age > 18' | jsmql prints { age: { $gt: 18 } }. Compiles JSMQL to MQL in a shell — handy for LLM coding tools.

Get started

Install

$ npm install @koresar/jsmql

  • Playground — JSMQL on the left, its MQL on the right.

Read

  • Language reference — the syntax, what each piece compiles to, and where it departs from JavaScript.
  • Worked examples — complete queries, each asserted against the exact MQL it produces.
  • Language rules — the promises the compiler keeps, including that the server never receives MQL that would fail.