Skip to content

Querying

Lookups

Append __lookup to a field name. Supported operators:

startswith, contains, icontains, gt, gte, lt, lte, in, nin, exists, isnull, range (inclusive [min, max]).

ts
User.objects.filter({ username__startswith: "ab" });
User.objects.filter({ salary__range: [40000, 80000] });
User.objects.filter({ supervisor__isnull: true });

Filter keys are fully typed: your editor autocompletes valid field__lookup keys for a model, checks each value against the field's type, and flags typos — salary__gte expects a number, username__startswith a string. _id is always available, and createdAt / updatedAt keys appear on models with timestamps: true. Regex lookups escape their input, and only the operators above are recognized — arbitrary MongoDB operators can't be injected through filter keys.

Operator-injection guard

Filter values containing MongoDB operator keys ($-prefixed) — the shape untrusted request data takes in a NoSQL injection — are rejected by default. If a trusted caller needs to pass raw operators, opt out per client:

ts
const dorm = new DORMClient(uri, dbName, { allowRawOperators: true });

Typing note (deep relational keys)

The value on a deep relational key like assignee__email__icontains is accepted loosely (typed as unknown) — ref__… keys go through a relational passthrough, so you won't get autocomplete/value-checking on the nested part the way you do for a top-level field like email__icontains. It compiles and runs correctly; it's just not strictly typed yet (a roadmap item).

Relational traversal

Chain __ through f.ref fields to join across collections. Register both models on the same client so DORM can resolve the join targets.

ts
// assets whose assignee's supervisor earns >= 50k — one filter object
await Asset.objects.filter({ assigned_to__supervisor__salary__gte: 50000 });

Traversal uses an inner join ($unwind), so documents whose reference is null/unmatched are excluded. Filter values for _id and f.ref / f.objectId fields are auto-coerced from hex strings to ObjectId, so filter({ _id: req.params.id }) just works.

Chaining (QuerySet)

all(), filter(), and exclude() return a lazy, chainable QuerySet that executes only on a terminal operation. Because a QuerySet is awaitable, await Model.objects.filter({...}) still resolves to the array — chaining is purely additive.

ts
const page = await Task.objects
  .filter({ done: false })
  .exclude({ priority__gte: 4 })
  .order_by("-priority", "title") // `-` = descending
  .offset(20)
  .limit(10);

await User.objects.filter({ active: true }).first();   // one doc or null
await Task.objects.filter({ done: false }).exists();   // boolean
await Task.objects.filter({ done: false }).count();    // number
await Task.objects.filter({ done: false }).select("title", "priority"); // projection
await Post.objects.filter({ id }).populate("author");  // hydrate a ref

// queryset-level writes use the chained filter:
await Task.objects.filter({ done: true }).update({ archived: true });
await Task.objects.exclude({ starred: true }).delete();
ChainingTerminal
filter(where) · exclude(where)await → documents
order_by(...fields)first() → doc | null
limit(n) · offset(n)exists() → boolean
select(...fields) · populate(...refs)count() · get(where?)
session(s)update(patch) · delete()

exclude() supports a single relational (ref) hop — e.g. exclude({ author__name: "spam" }) removes posts whose author matches, while keeping posts with no author (Django semantics). Multi-hop relational excludes aren't supported yet.

Released under the MIT License.