All questions

Mini Object-relational Mapper III

Premium

Mini Object-relational Mapper III

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.

Signature

// 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.

Examples

// 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
//   ]

Notes

  • Execution is deferred until 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.
  • Avoid N+1. The obvious eager loader scans the related table once per parent row — N parents trigger N scans (plus the original, hence "N+1"). You must group the related rows by their foreign key exactly once, then look each parent up in that index. The grader checks that the related table is grouped once, not re-scanned per parent.
  • Never mutate the source tables. Return brand-new row objects (and new included child objects). A caller mutating a returned row must not change the original data.
  • 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 }.
  • Don't worry about nested includes (a post's author's 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.

Upgrade to Premium