{"id":50774487,"url":"https://github.com/flash-oss/jsmql","last_synced_at":"2026-06-11T22:01:09.799Z","repository":{"id":357863137,"uuid":"1238829153","full_name":"flash-oss/jsmql","owner":"flash-oss","description":"Write MongoDB aggregation expressions in JavaScript. JS-subset language that compiles to MQL JSON.","archived":false,"fork":false,"pushed_at":"2026-06-04T10:59:06.000Z","size":6366,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-04T12:21:38.504Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://flash-oss.github.io/jsmql/playground.html","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flash-oss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-14T13:48:31.000Z","updated_at":"2026-06-04T10:50:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/flash-oss/jsmql","commit_stats":null,"previous_names":["flash-oss/jsmql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/flash-oss/jsmql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flash-oss%2Fjsmql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flash-oss%2Fjsmql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flash-oss%2Fjsmql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flash-oss%2Fjsmql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flash-oss","download_url":"https://codeload.github.com/flash-oss/jsmql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flash-oss%2Fjsmql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34219510,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-11T22:01:08.336Z","updated_at":"2026-06-11T22:01:09.781Z","avatar_url":"https://github.com/flash-oss.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# jsmql\n\n**Write MongoDB aggregation queries in JavaScript.** A strict JS subset that compiles to MQL JSON — like SQL but for MongoDB, using the syntax you already know.\n\n```js\nimport { jsmql } from \"@koresar/jsmql\";\n\n// Filter — for db.coll.find(filter). No `;` at top level.\nconst age = 18;\nlet filter = jsmql`$.age \u003e ${age} \u0026\u0026 $.status === \"active\"`\n// → { age: { $gt: 18 }, status: \"active\" }   ← index-friendly query doc\n\n// Pipeline — for db.coll.aggregate(pipeline). Any `;` flips to stage mode.\n// Snapshot one user, then pivot the stream onto their 5 most-recent orders.\nlet pipeline = jsmql`\n  $$ = $$.filter(u =\u003e u.email === \"me@example.com\").slice(0, 1);\n  let userId = $._id;\n  $$ = $$$$.archive.orders\n    .filter(o =\u003e o.userId === userId)\n    .toSorted((a, b) =\u003e a.placedAt - b.placedAt)\n    .toReversed()\n    .slice(0, 5);\n`;\n// → [\n//   { $match: { email: \"me@example.com\" } },\n//   { $limit: 1 },\n//   { $set: { \"__jsmql.userId\": \"$_id\" } },\n//   { $lookup: { from: { db: \"archive\", coll: \"orders\" }, let: { userId: \"$__jsmql.userId\" }, pipeline: [\n//       { $match: { $expr: { $eq: [\"$userId\", \"$$userId\"] } } },\n//       { $sort: { placedAt: -1 } },\n//       { $limit: 5 },\n//   ], as: \"__jsmql.__lookup1\" } },\n//   { $unwind: \"$__jsmql.__lookup1\" },\n//   { $replaceWith: \"$__jsmql.__lookup1\" },\n//   { $unset: \"__jsmql\" }\n// ]\n\n// Raw expression — for inside a stage body, or db.coll.updateOne(filter, update).\nlet expr = jsmql.expr(($) =\u003e $.items.map((i) =\u003e i.price * i.qty).reduce((a, x) =\u003e a + x, 0))\n// → { $reduce: { input: { $map: { input: \"$items\", as: \"i\",\n//     in: { $multiply: [\"$$i.price\", \"$$i.qty\"] } } },\n//   initialValue: 0, in: { $add: [\"$$value\", \"$$this\"] } } }\n```\n\n**MongoDB 8.0 deprecated server-side JavaScript via `$function`, `$accumulator`, and `$where`.** The JSMQL is the replacement: native MQL, no `--noscripting` issues, index-friendly, IDE-aware, testable as plain JS.\n\n## Install\n\n```sh\nnpm install @koresar/jsmql\n```\n\nESM + CJS, runs in browsers, zero dependencies. Works with **Node 14+**, Deno, and Bun.\n\n## Tour\n\n```js\nimport \"@koresar/jsmql/ops\";          // ambient $-prefixed globals — autocomplete for 182 MQL ops \u0026 every stage\nimport { jsmql } from \"@koresar/jsmql\";\n\n// Arrow form — your prettier/oxfmt handles formatting.\n// No `;` at top level → query Filter (the doc db.coll.find(filter) takes).\njsmql(($) =\u003e $.email === $.email.trim().toLowerCase().endsWith(\"@flash-payments.com\"))\n// → {\"$expr\":{\"$eq\":[\"$email\",{\"$eq\":[{\"$substrCP\":[{\"$toLower\":{\"$trim\":{\"input\":\"$email\"}}},{\"$subtract\":[{\"$strLenCP\":{\"$toLower\":{\"$trim\":{\"input\":\"$email\"}}}},{\"$strLenCP\":\"@flash-payments.com\"}]},{\"$strLenCP\":\"@flash-payments.com\"}]},\"@flash-payments.com\"]}]}}\n\n// Pipelines — any `;` flips to stage mode (the array db.coll.aggregate(pipeline) takes).\njsmql(($) =\u003e {\n  $match($.age \u003e= 18 \u0026\u0026 $.region === \"AU\");      // → query doc, indexes still work\n  $group({ _id: $.shopId, total: { $sum: $.amount } });\n  $sort({ total: -1 });\n});\n// → [{ \"$match\": { \"age\": { \"$gte\": 18 }, \"region\": \"AU\" } }, { \"$group\": { \"_id\": \"$shopId\", \"total\": { \"$sum\": \"$amount\" } } }, { \"$sort\": { \"total\": -1 } }]\n\n// Use `?.` where a field might be null — you get `$ifNull` guards exactly there:\njsmql('[...$.mods, ...$.room?.mods, \"root\"].includes($.userId)')\n//  { \"$expr\": { \"$in\": [\"$userId\", { \"$concatArrays\": [\"$mods\", { \"$ifNull\": [\"$room.mods\", []] }, [\"root\"]] }] } }\n\n// `new Date(...)` with literal args folds to a real JS Date — index-friendly query doc:\njsmql(`$.method === \"postalDelivery\" \u0026\u0026 $.createdAt \u003e= new Date(\"2026-01-01\")`)\n// → { method: \"postalDelivery\", createdAt: { $gte: \u003cDate 2026-01-01\u003e } }\n// `new Date()` and `new Date($.field)` still need server-time evaluation and ride in $expr.\n\n// Template-tag — interpolate runtime literals from outer scope\nconst ids = [1, 2, 3];\njsmql`$.status === \"open\" \u0026\u0026 $.id in ${ids}`\n// → { \"status\": \"open\", \"$expr\": { \"$in\": [\"$id\", [1, 2, 3]] } }\n\n// jsmql.compile — parse once, bind many. Output stays index-friendly.\nconst eligible = jsmql.compile(({ minAge, region }, $) =\u003e {\n  $match($.age \u003e= minAge \u0026\u0026 $.region === region);\n  $project({ age: 1, email: 1, address: 1 });\n});\neligible({ minAge: 21, region: \"AU\" });\n// → [{\"$match\":{\"age\":{\"$gte\":21},\"region\":\"AU\"}},{\"$project\":{\"age\":1,\"email\":1,\"address\":1}}]\n\n// JS-natural `=`, `+=`, `delete` compile to coalesced $set / $unset\njsmql(($) =\u003e {\n  $.score += 1;\n  delete $.tempToken;\n  $.status = \"done\";\n});\n// → [{ \"$set\": { \"score\": { \"$add\": [\"$score\", 1] } } }, { \"$unset\": \"tempToken\" }, { \"$set\": { \"status\": \"done\" } }]\n\n// Assigning to bare `$` replaces the whole document — lowers to $replaceWith\njsmql(`$match($.profile != null); $ = $.profile; $ = { ...$, score: $.points * 1.1 }`);\n// → [\n//     { \"$match\": { \"profile\": { \"$ne\": null } } },\n//     { \"$replaceWith\": \"$profile\" },\n//     { \"$replaceWith\": { \"$mergeObjects\": [\"$$ROOT\", { \"score\": { \"$multiply\": [\"$points\", 1.1] } }] } }\n//   ]\n\n// Multi-facet aggregation — every value a `$$.filter(...)` lowers to one $facet stage\njsmql(`$ = {\n  topByScore: $$.filter(o =\u003e { $sort({ score: -1 }); $limit(10); }),\n  recent:     $$.filter(o =\u003e o.createdAt \u003e= \"2026-01-01\"),\n  byStatus:   $$.filter(o =\u003e { $group({ _id: o.status, n: $sum(1) }); })\n}`);\n// → [{ \"$facet\": {\n//       \"topByScore\": [{ \"$sort\": { \"score\": -1 } }, { \"$limit\": 10 }],\n//       \"recent\":     [{ \"$match\": { \"createdAt\": { \"$gte\": \"2026-01-01\" } } }],\n//       \"byStatus\":   [{ \"$group\": { \"_id\": \"$status\", \"n\": { \"$sum\": 1 } } }]\n//   } }]\n\n// Top 10 users by revenue: $group the orders, then sort descending and take the first 10.\n// The `$$ = $$.toSorted(...).slice(...)` chain lowers to $sort + $limit.\njsmql(`\n$group({ _id: $.userId, revenue: $sum($.total), orders: $sum(1) });\n$$ = $$.toSorted((a, b) =\u003e b.revenue - a.revenue).slice(0, 10);\n`);\n// [\n//     { \"$group\": { \"_id\": \"$userId\", \"revenue\": { \"$sum\": \"$total\" }, \"orders\": { \"$sum\": 1 } } },\n//     { \"$sort\": { \"revenue\": -1 } },\n//     { \"$limit\": 10 }\n// ]\n\n// `jsmql()` returns an UpdateFilter as a pipeline, to avoid common footgun of wiping out the whole collection.\ndb.users.updateMany({}, jsmql(($) =\u003e $.name = $.name.toUpperCase()))\n// → [{ \"$set\": { \"name\": { \"$toUpper\": \"$name\" } } }] -\u003e will upper-case all names in the collection\n\n// `jsmql.expr()` returns a partial MQL JSON. Won't protect from the same footgun.\ndb.users.updateMany({}, jsmql.expr(($) =\u003e $.name = $.name.toUpperCase()))\n// → { \"$set\": { \"name\": { \"$toUpper\": \"$name\" } } } -\u003e will WIPE OUT all names in the collection\n\n// Strict-shape entry points — throw if the input would produce the wrong shape.\n// Use these when the call site demands a specific shape and a silent\n// mis-dispatch would be a footgun.\ndb.users.find(jsmql.filter(\"$.age \u003e 18\"));            // throws on Pipeline-shaped input\ndb.users.aggregate(jsmql.pipeline(\"$match($.age \u003e 18); $sort({ age: 1 })\")); // throws on bare expressions\ndb.users.updateOne({ _id: 1 }, jsmql.update(\"$.name = $.name.toUpperCase()\"));\n// update() additionally rejects any stage outside MongoDB's update-pipeline\n// whitelist ($addFields, $project, $replaceRoot, $replaceWith, $set, $unset),\n// so a misplaced `$match` is caught at compile time instead of at the server.\n\n// Raw expression — for embedding inside a hand-written stage body\nconst stage = { $addFields: { discount: jsmql.expr(($) =\u003e $.price * (1 - $.loyalty.multiplier)) } }\n// → { $addFields: { discount: { $multiply: [\"$price\", { $subtract: [1, \"$loyalty.multiplier\"] }] } } }\n\n// Escape hatch — call any MongoDB operator as a function - $dateTrunc in this case\njsmql.expr(($) =\u003e $set({ createdAtWeek: $dateTrunc({ date: $.createdAt, unit: \"week\" }) }))\n// → { $set: { \"createdAtWeek\": { \"$dateTrunc\": { \"date\": \"$createdAt\", \"unit\": \"week\" } } } }\n\njsmql(($) =\u003e $.age = 18); // generates a pipeline, to make sure you can use this in updateOne(), updateMany(), etc\n// → [{ \"$set\": { \"age\": 18 } }]\njsmql.expr(($) =\u003e $.age = 18); // generates an partial expression, to use within OTHER aggregation or filter expressions\n// → { \"$set\": { \"age\": 18 }\n\n// Validate without throwing — every error carries { message, pos, code }\njsmql.validate(($) =\u003e $.age \u003e 18)\n// → { valid: true, errors: [] }\n```\n\nThe **[live playground](https://flash-oss.github.io/jsmql/playground.html)** is the best place to see dozens of other JSMQL examples.\n\n## Why the arrow form\n\nThe arrow function is **never executed** — jsmql() calls `Function.prototype.toString()` on it, strips the parameter list, and parses the body. That single trick gives you:\n\n- **Formatting for free.** Prettier, oxfmt, and every other JS formatter indent and line-break your query like any other JavaScript. No jsmql plugin, no custom config.\n- **Linting for free.** ESLint, Biome, and your editor's TypeScript service see real JS — they flag typos, unused identifiers, and shape mismatches at write time.\n- **Code completion.** With `import \"@koresar/jsmql/ops\"`, your IDE autocompletes every stage and operator name, suggests the argument keys from the official MongoDB MQL spec, and surfaces the operator's description on hover. It also declares the `$$` / `$$$` / `$$$$` context-ref prefixes — so arrow-form code using them type-checks, with full completion and annotated option objects for the diagnostic source stages (`$$.collStats({…})`, `$$$$.currentOp({…})`, …).\n- **AI coding works out of the box.** Copilot, Cursor, and Claude already know JavaScript — they autocomplete jsmql idiomatically because jsmql *is* JavaScript. There is no new vocabulary for them to learn.\n- **Pre-compilation.** jsmql.compile() parses once, executes many times.\n\n## Highlights\n\n- **JS you already know** — operators, ternaries, template literals, optional chaining, spread, computed keys, numeric separators, `Math.*`, `Date`, `typeof`, `instanceof`, comments. If `node --check` accepts it, jsmql does too.\n- **182 operators, full coverage** — every aggregation expression and accumulator from the official MongoDB MQL spec, including Bitwise and Window categories. Unknown operators pass through, so new MongoDB releases work day one.\n- **Plain MQL passes through.** Drop hand-written MQL JSON inline — `{ $gt: [\"$age\", 18] }`, a whole stage, a whole pipeline — and jsmql compiles it to itself. Mix the two freely, migrate one expression at a time, or paste verbatim from the MongoDB docs.\n- **Filter vs Pipeline picked automatically** — a stage call (`$match(...)`, `$project(...)`, …), an update op (`$.x = …`), or a statement-position array mutator (`$.tags.sort()`, `$.events.reverse()`) at the top level lowers as a `Pipeline` (`db.coll.aggregate(pipeline)` / `db.coll.updateOne(filter, update)`); any `;`-separated input lowers as a multi-stage Pipeline; everything else lowers as a `Filter` (`db.coll.find(filter)`). Index-safe predicates translate to query-document form so existing indexes still get used.\n- **Joins as JS** — `$$$.\u003ccoll\u003e.find(pred)` / `$$$.\u003ccoll\u003e.filter(pred)` lower to `$lookup` stages. `.find()` follows JS semantics — returns one doc or null; `.filter()` keeps the array. Chained `.length`, `.reduce(fn, init)`, and member access compose inline. Block-body lambdas (`o =\u003e { $match(...); $sort(...); $limit(N); }`) become the full sub-pipeline body. `$$$$.\u003cdb\u003e.\u003ccoll\u003e.find/filter(pred)` covers cross-database joins (requires Atlas Data Federation). Indexes still get used when the predicate is a simple field-to-field equality. See [docs/LANGUAGE.md → Cross-collection lookups](docs/LANGUAGE.md#cross-collection-lookups-coll-find--filter).\n- **Collection unions as `Array.push`** — `$$.push({...}, ...$$$.\u003ccoll\u003e.filter(pred), $$$.\u003cother\u003e.find(pred))` lowers to `$unionWith` stages. The spread (`...`) rule is JS-faithful: `.filter` and bare collections are arrays so they must be spread; `.find` and inline objects are scalars so they must not. `$$$$.\u003cdb\u003e.\u003ccoll\u003e` works for cross-database union (same Atlas caveat as cross-DB lookups). See [docs/LANGUAGE.md → Collection union](docs/LANGUAGE.md#collection-union-push).\n- **Replace root as JS assignment** — `$ = \u003cexpr\u003e` lowers to `$replaceWith`: lift a sub-document (`$ = $.profile`), merge fresh fields (`$ = { ...$, score: ... }` — bare `$` is the current document, like MQL's `$$ROOT`), or pivot to a joined doc (`$ = $$$.users.find(pred)`). If the RHS clearly isn't a document (array literal, scalar, `.filter()` lookup), you get a compile-time error pointing at the fix. See [docs/LANGUAGE.md → Replace root via `$ = \u003cexpr\u003e`](docs/LANGUAGE.md#replace-root-via--expr).\n- **Materialised views via `$out`** — `$$$.\u003ccoll\u003e = $$` and `$$$$.\u003cdb\u003e.\u003ccoll\u003e = $$` lower to a `$out` stage: the LHS names the destination, the RHS names the (optionally filtered) source. An inline `$$.filter(\u003cpredicate\u003e)` on the RHS emits a `$match` before the write (`$$$$.dw.archive = $$.filter(u =\u003e !u.active)` → `[{ $match: … }, { $out: { db: \"dw\", coll: \"archive\" } }]`). Bracket form (`$$$[\"my-coll.v2\"] = $$`) addresses collection names that aren't valid JS identifiers. The compiler enforces \"`$out` must be last\" at compile time. See [docs/LANGUAGE.md → `$out`](docs/LANGUAGE.md#out-write-the-pipeline-to-a-collection).\n- **Diagnostics scoped by prefix** — MongoDB's system source stages are method calls on the context ref whose scope they require: `$$.indexStats()` / `$$.collStats({…})` / `$$.planCacheStats()` / `$$.listSearchIndexes({…})` run on the collection (`db.coll.aggregate()`); `$$$$.currentOp({…})` / `$$$$.listSessions({…})` / `$$$$.listLocalSessions({…})` / `$$$$.listSampledQueries({…})` / `$$$$.shardedDataDistribution()` run on the deployment (admin DB). Each lowers to its `$`-stage as the pipeline's first stage; using one at the wrong scope is a compile-time error that names the right prefix. See [docs/LANGUAGE.md → System / diagnostic stages](docs/LANGUAGE.md#system--diagnostic-stages-indexstats-currentop-).\n- **`$facet` as a named object of filters** — when every value of `$ = { … }` is a `$$.filter(\u003clambda\u003e)`, the same surface lowers to one `$facet` stage with each entry as a named sub-pipeline. Inside each lambda, the param is the input document (use `o.\u003cfield\u003e`, not `$.\u003cfield\u003e`); use an expression body for a simple predicate or a block body to run a full pipeline of stages. See [docs/LANGUAGE.md → $facet via `$ = { key: $$.filter(p), … }`](docs/LANGUAGE.md#facet-via---key-filterp-).\n- **Replace stream as JS assignment** — `$$ = \u003cexpr\u003e` reshapes the whole document stream the same way `$ = \u003cexpr\u003e` reshapes one document. Narrow it (`$$ = $$.filter(t =\u003e t.client === 156)` → `$match`), switch source to another collection (`$$ = $$$.archive.filter(p)` → `$limit: 0` + `$unionWith`), or pivot each input doc onto a correlated collection (`$$ = $$$.orders.filter(o =\u003e o.userId === $._id)` → `$lookup` + `$unwind` + `$replaceWith`). jsmql picks the lowering from the predicate shape: a predicate that references an outer field auto-selects `$lookup`, the only stage that can thread an outer-doc snapshot into its sub-pipeline (`$unionWith` has no `let:`). Outer `let` bindings hoist into `$lookup.let` automatically. See [docs/LANGUAGE.md → Replace stream](docs/LANGUAGE.md#replace-stream-via--expr).\n- **Stream methods chain on the RHS** — after the `$$` / `$$$.\u003ccoll\u003e` receiver (optionally `.filter(p)`), chain JS array methods and each appends stages: `.slice(start, end?)` → `$skip` / `$limit`, `.toSorted((a, b) =\u003e …)` → `$sort`, `.toReversed()` flips the preceding sort, `.map(d =\u003e …)` → `$replaceWith`, `.flatMap(d =\u003e d.path)` → `$unwind`, `.concat(...)` → `$unionWith`. A `reduce` whose seed is `[]` already returns a stream, so it assigns directly — `$$ = $$.reduce((acc, d) =\u003e acc.concat(d.contactDetails), [])` → `$replaceWith` (fold-to-summary uses the `$$ = [{ total: $$.reduce(…) }]` form → `$group`). For a bare `$$` receiver the `$$ =` head is optional — `$$.filter(o =\u003e o.tier === \"gold\").map(d =\u003e ({ id: d._id }));` works as a plain statement, identical to (and splittable into) the assignment form. See [docs/LANGUAGE.md → Stream methods](docs/LANGUAGE.md#stream-methods-chained-after-the-rhs).\n- **JS array mutators mutate at statement position** — `$.events.sort(e =\u003e e.t)`, `$.events.push(x)`, `$.events.pop()`, `.shift()`, `.unshift(...)`, `.reverse()`, `.splice(...)`, `.fill(...)` desugar to a `$set` stage that reassigns the field. `.toSorted()`, `.toReversed()`, `.toSpliced(...)`, `.with(...)` keep returning a new array. Sort key functions (`.toSorted(e =\u003e e.distance)`, `.toSorted(e =\u003e -e.distance)`) lower to MongoDB's `sortBy: { path: ±1 }` shape — including nested paths. Mutators in expression position throw with both the immutable variant and the statement-position option called out. See [docs/LANGUAGE.md → Mutators](docs/LANGUAGE.md#mutators-at-statement-position-they-mutate-the-field).\n- **Three call shapes** — arrow `jsmql(($) =\u003e …)`, string `jsmql(\"…\")`, and template tag `` jsmql`…${val}…` `` for embedding outer-scope values.\n- **Polymorphic by default, strict on demand** — `jsmql()` picks Filter or Pipeline from the input; `jsmql.filter()`, `jsmql.pipeline()`, and `jsmql.update()` lock it to one shape and throw an actionable error otherwise (with the offending stage named, for `update()`). `jsmql.compile(fn)` parses once for parameterised parse-once-bind-many. `jsmql.expr()` returns the raw aggregation expression that drops into a stage body. The three call shapes (string / arrow / template tag) apply to all of them.\n- **`@koresar/jsmql/ops`** — a pure-types side-effect import that adds ambient `$match` / `$dateAdd` / … globals. Zero runtime cost; bundlers tree-shake it to nothing.\n- **Pre-flight validation** — jsmql rejects the pipeline mistakes the MongoDB server would otherwise reject, at compile time: stage placement (`$out`/`$merge` must be last, `$collStats`/`$geoNear`/`$changeStream` and friends must be first, stages forbidden inside `$facet`/`$lookup`/`$unionWith`), stage-body shape (literal type/range/enum/required-key/mutual-exclusivity rules — `$limit(-5)`, `$count('')`, `$project` mixing include/exclude, `$bucket` boundaries out of order, a `$merge` `whenMatched` typo), and `$match` query placement (`$text` must be first; `$near`/`$where` aren't allowed). Only 100%-certain violations throw — a value jsmql can't evaluate (`$limit($.n)`) or a deployment-dependent rule (sharding, memory limits, Atlas availability) still emits MQL. See [docs/LANGUAGE.md → Mistakes caught at compile time](docs/LANGUAGE.md#mistakes-caught-at-compile-time).\n- **Actionable errors** — every error names the construct, suggests the nearest valid name (`Did you mean '…'?`), and carries a real `.pos` so editors can underline the offending region.\n- **Strict TS, strippable source** — runs as-is on Node 22.18+ / 24.3+, Deno, and Bun (no flags, no transpile).\n- **`jsmql` on the command line** — a `jq`-style bin: JSMQL on stdin, MQL JSON on stdout. `echo '$.age \u003e 18' | jsmql`. Opt-in `--filter` / `--pipeline` / `--expr` / `--update` / `--validate`, `--compact`, and jq-style `--arg` / `--argjson` for parameterised arrows. See [Command line](#command-line-jsmql).\n\n## Using jsmql with mongoose\n\nA one-shot registration patches the `Model` static methods so the standard `find / updateOne / aggregate / …` calls accept jsmql source directly, alongside the plain MQL-JSON forms you already pass them:\n\n```js\nconst mongoose = require(\"mongoose\");\nrequire(\"@koresar/jsmql/mongoose\")(mongoose);\n// or, ESM: import jsmqlMongoose from \"@koresar/jsmql/mongoose\"; jsmqlMongoose(mongoose);\n\nconst User = mongoose.model(\"User\", new mongoose.Schema({ name: String, age: Number, score: Number }));\n\nUser.find(\"$.age \u003e 18\");                            // → find({ age: { $gt: 18 } })\nUser.find(($) =\u003e $.age \u003e 18 \u0026\u0026 $.region === \"AU\"); // → find({ age: { $gt: 18 }, region: \"AU\" })\n\nUser.updateMany({}, ($) =\u003e $.score += 1);\n// → updateMany({}, [{ $set: { score: { $add: [\"$score\", 1] } } }])\n\nUser.aggregate(($) =\u003e {\n  $match($.status === \"active\");\n  $group({ _id: $.region, total: { $sum: $.amount } });\n  $sort({ total: -1 });\n});\n\nUser.find({ age: { $gt: 18 } });                    // plain MQL JSON still passes through untouched\n```\n\n**Detection rule.** A patched argument is treated as jsmql source only when it's a **string** or a **function**. Plain objects/arrays (the regular MQL JSON forms) pass through to mongoose unchanged, so existing call sites need no migration. Template-tag inputs (`jsmql\\`…\\``) lower to an object at the user's call site, so they take the pass-through path too.\n\n**TypeScript.** The plugin ships a `declare module \"mongoose\"` augmentation that adds JSMQL-shaped overloads (`string | JsmqlFn`) to every patched `Model` static, so `User.find(\"$.age \u003e 18\")` and `User.aggregate(($) =\u003e …)` type-check after `import \"@koresar/jsmql/mongoose\"` — no per-call cast required. Mongoose's own `FilterQuery\u003cT\u003e` / `UpdateQuery\u003cT\u003e` overloads still apply on the MQL-JSON pass-through path.\n\n**Patched methods** (with the slot used): `find` / `findOne` / `findOneAnd{Delete,Replace,Update}` / `countDocuments` / `deleteOne` / `deleteMany` / `replaceOne` / `exists` (filter at 0), `updateOne` / `updateMany` / `findOneAndUpdate` / `findByIdAndUpdate` (update at 1), `distinct` (filter at 1), `aggregate` (pipeline at 0). Each slot lowers through the matching strict-shape entry (`jsmql.filter` / `jsmql.update` / `jsmql.pipeline`), so a wrong-shape input — e.g. a bare expression at an `aggregate` slot — throws with the actionable strict-mode error at the patched call site instead of silently going wrong server-side. Registering twice on the same `mongoose` is a no-op.\n\nSee [docs/specs/mongoose-plugin.md](docs/specs/mongoose-plugin.md) for the full per-slot table, the methods that are deliberately *not* patched (e.g. `findOneAndReplace`'s replacement document), and the idempotence / subclass-propagation contracts.\n\n## Command line (`jsmql`)\n\nInstalling the package puts a `jsmql` command on your `PATH`. It works like `jq`: **JSMQL source on stdin, MQL JSON on stdout** (a positional argument or `--file \u003cpath\u003e` also work as the source).\n\n```sh\necho '$.age \u003e 18' | jsmql\n# {\n#   \"age\": { \"$gt\": 18 }\n# }\n\necho '$match($.age \u003e 18); $sort({ age: -1 })' | jsmql --pipeline -c\n# [{\"$match\":{\"age\":{\"$gt\":18}}},{\"$sort\":{\"age\":-1}}]\n\njsmql --expr '$.price * (1 - $.discount)'\n# { \"$multiply\": [\"$price\", { \"$subtract\": [1, \"$discount\"] }] }\n```\n\nWith no flag the output shape is picked the same way `jsmql()` picks it (a top-level `;` makes it a Pipeline). The strict flags lock the shape and inherit the library's actionable errors:\n\n| Flag | Shape | Library entry |\n| --- | --- | --- |\n| *(none)* | Filter or Pipeline | `jsmql()` |\n| `--filter` | Filter document | `jsmql.filter()` |\n| `--pipeline` | stage array | `jsmql.pipeline()` |\n| `--expr` | aggregation expression | `jsmql.expr()` |\n| `--update` | update pipeline | `jsmql.update()` |\n| `--validate` (`--check`) | `{ valid, errors }`; exit 1 if invalid | `jsmql.validate()` |\n\nFormatting is pretty 2-space by default (like `jq`); use `-c`/`--compact`, `--tab`, or `--indent N`. Parameterise a query with jq's own flags — the source must then be a parameterised arrow:\n\n```sh\necho '({ minAge }, $) =\u003e $.age \u003e minAge' | jsmql --argjson minAge 18\n# { \"age\": { \"$gt\": 18 } }\n```\n\n`--arg name value` binds a string; `--argjson name value` binds a JSON value. Errors print compiler-style with a caret at the offending position; exit codes are `0` success, `1` compile error / invalid, `2` usage error. `jsmql --help` lists everything. Full reference: [docs/specs/cli.md](docs/specs/cli.md).\n\n## Try it \u0026 learn more\n\n- **[Live playground](https://flash-oss.github.io/jsmql/playground.html)** — write jsmql, see the MQL JSON update live. Pre-loaded with real-world recipes: tiered discounts, slug generation, audit logs, pivot tables, parameterised reports, and more.\n- **[docs/LANGUAGE.md](docs/LANGUAGE.md)** — the full language reference: every operator, every method, update-filter rules, `$match` query translation, `jsmql.compile` parameter semantics, `jsmql.expr` for raw aggregation expressions, the strict-shape entry points (`jsmql.filter` / `jsmql.pipeline` / `jsmql.update`), the `@koresar/jsmql/ops` import, error catalogue, server-side-JS migration guide.\n- **[docs/DEVLOG.md](docs/DEVLOG.md)** — the running record of language decisions and the reasoning behind them.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflash-oss%2Fjsmql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflash-oss%2Fjsmql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflash-oss%2Fjsmql/lists"}