Build a small in-memory query builder over an array of plain-object rows, inspired by how Drizzle ORM reads: query(rows).where(...).orderBy(...).limit(...).all(). The chain reads top-to-bottom like a sentence, and nothing actually runs until the terminal .all() call. Along the way you'll write small operator helpers — eq, gt, and, or — that build the predicates .where() filters on. A predicate here is just a function that takes one row and returns true or false.
// query(rows) returns a chainable, lazy builder. Build steps accumulate;
// .all() executes filter → sort → offset → limit and returns a new array.
query(rows: object[]): Builder
interface Builder {
where(cond: (row) => boolean): Builder; // chain multiple → AND
orderBy(key: string, dir?: 'asc' | 'desc'): Builder; // dir defaults to 'asc'
limit(n: number): Builder;
offset(n: number): Builder;
all(): object[]; // terminal — runs the plan
}
// Operator helpers each return a predicate (row) => boolean:
eq(key, value) ne(key, value)
gt(key, value) gte(key, value)
lt(key, value) lte(key, value)
and(...conds) or(...conds) // combine predicates into one predicate
query is also exported under the alias drizzleQueryBuilder.
const users = [
{ id: 1, name: 'Ada', age: 36, role: 'admin' },
{ id: 2, name: 'Bo', age: 22, role: 'user' },
{ id: 3, name: 'Cy', age: 41, role: 'user' },
{ id: 4, name: 'Di', age: 22, role: 'admin' },
{ id: 5, name: 'Eve', age: 30, role: 'user' },
];
// Filter to users, sort oldest-first, take the second page of size 1.
query(users)
.where(eq('role', 'user'))
.orderBy('age', 'desc')
.offset(1)
.limit(1)
.all();
// → [{ id: 5, name: 'Eve', age: 30, role: 'user' }]
// role=user → ids [2,3,5]; by age desc → [3,5,2]; offset 1 → [5,2]; limit 1 → [5].
// A compound predicate: adults under 35, in one where().
query(users).where(and(gt('age', 18), lt('age', 35))).all();
// → rows for Bo (22), Di (22), Eve (30)
// or() keeps a row if any branch matches.
query(users).where(or(eq('age', 41), eq('age', 36))).all();
// → rows for Ada (36), Cy (41)
.offset(4).limit(2) means "skip 4, then take 2.".where() AND-combines. Two .where() calls keep only rows passing both. To express OR within a single filter, use the or(...) helper.orderBy direction defaults to 'asc'. Chaining multiple orderBy calls makes the first key primary and later keys tie-breakers..all() returns a new array; the rows you passed in keep their original order and contents. Sorting must happen on a copy.limit larger than the row count returns whatever is available; offset past the end returns []..all() runs it. Calling .all() twice on the same builder gives the same result.count, sum), or compiling to real SQL — those are listed under Going further in the solution.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.