Build a small in-memory ORM — an object-relational mapper, the layer libraries like Prisma, ActiveRecord, and Django's ORM put between your code and rows of data. Yours holds plain JavaScript arrays of objects (the "tables") and lets a caller build a query by chaining methods: filter rows with where, narrow which fields come back with select, and pull in related rows from another table with include. The interesting part is include: fetching every user and their posts in one go, the way a real ORM eager-loads associations — without re-scanning the posts table once per user.
// Declare your data and how the tables relate, then query.
const orm = createOrm({
tables: {
users: [{ id, name, ... }, ...],
posts: [{ id, userId, title }, ...],
},
relations: {
users: { posts: { hasMany: 'posts', foreignKey: 'userId' } },
posts: { author: { belongsTo: 'users', foreignKey: 'userId' } },
},
});
orm.query('users') // start a query on a table
.where(row => boolean) // keep rows where pred is true; chaining ANDs
.select(['id', 'name']) // project: only these fields (+ included relations)
.include('posts') // eagerly attach related rows under this key
.all(); // execute → Array<new row object>
A hasMany relation attaches an array of children under the relation key. A belongsTo relation attaches a single object (or null when no parent matches). foreignKey is the field on the child row that points at the parent's id.
// Projection: select narrows the returned fields.
orm.query('users').select(['id', 'name']).all();
// → [ { id: 1, name: 'Ada' }, { id: 2, name: 'Bao' }, ... ]
// 'role' and every other column are dropped.
// include hasMany: each user gets an array of their posts.
orm.query('users').include('posts').all();
// → [
// { id: 1, name: 'Ada', role: 'admin', posts: [ { id: 10, userId: 1, title: 'Hello' }, ... ] },
// { id: 3, name: 'Cyd', role: 'member', posts: [] }, // no posts → empty array
// ]
// include belongsTo: each post gets its single author (or null).
orm.query('posts').include('author').all();
// → [
// { id: 10, userId: 1, title: 'Hello', author: { id: 1, name: 'Ada', role: 'admin' } },
// { id: 13, userId: 99, title: 'Orphan', author: null }, // no matching user
// ]
all(). where, select, and include only record intent and return the builder so calls chain. Nothing reads the tables until all() runs.hasMany returns an array; belongsTo returns one object or null. A user with no posts gets [], never null. A post whose userId matches no user gets author: null, never undefined and never a missing key.select projects the row's own fields only. Included relations are always attached under their relation key regardless of select — you select columns, not associations. So select(['name']).include('posts') returns { name, posts }.where filtering inside an included relation, ordering, pagination, or write operations — those are out of scope (see 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.