Proper way to include computed field in document

Hi guys discovering Meteor this weekend :slight_smile:

I’m building a todo app, any user can create its own todos. I display all the todos created by every users in a list. I want to display the todo’s owner username next to the todo content. What’s the proper way to achieve it?

server:

import { Meteor } from 'meteor/meteor';
import { Todos } from '/imports/db';

Meteor.methods({
  'todos.insert'(newTodo) {
    Todos.insert({
      userId: this.userId,
      createdAt: new Date(),
      done: false,
      content: newTodo,
    });
  },
  'todos.setDone'(todoId, newDone) {
    Todos.update(todoId, {
      $set: { done: newDone },
    });
  },
});

Meteor.publish('todos', function() {
  return Todos.find({});
});

UI (using Svelte):

<script>
  import { Todos } from '/imports/db';
  let todos = [];
  let newTodo = '';

  $m: todos = Todos.find({}).fetch();

  function createTodo() {
    Meteor.call('todos.insert', newTodo);
    newTodo = '';
  }

  function toggleDone(todo) {
    Meteor.call('todos.setDone', todo._id, !todo.done);
  }
</script>

<h1 class="text-2xl font-bold">Home</h1>
<h2 class="text-xl font-bold">Todos ({todos.length})</h2>

<ul>
  {#each todos as todo}
    <li>
      <label>
        <input
          type="checkbox"
          bind:checked={todo.done}
          on:change={() => toggleDone(todo)}
        />
        <span class:line-through={todo.done}>
          <!-- I want to replace the userId by the username -->
          {todo.userId} - {todo.content}
        </span>
      </label>
    </li>
  {/each}
</ul>

Maybe a dumb question but all the examples I found directly store the a copy of the username in the Todo document which is not very convenient.
Thanks!

Storing a copy of the username (and any other required data) is a typical NoSQL pattern to avoid joins and prepare data immediately for use by the UI.

EDIT: found another solution to experiment with:

Add a “transform” in the options for your query, or directly onto the Collection definition. When publishing the Todos, the transform can look up the username. It will be a bit slow, so caching it ahead of time is probably better but introduces other issues.

Whatever you do, use the Meteor dev tools to check that the client is only getting usernames sent to it and isn’t getting entire user records along with all sorts of sensitive information.

ORIGINAL:

What you would need to do without saving a copy of the username, would be (off the top of my head):

With collections and pub sub

  • there’s better ways to do this (I think you can make fake collections on the client side? Or add fields to an existing collection via the cursor?) but I’ll go with my naive solution
  • have a publication that mirrors whatever your todos publication might have, except instead of returning the cursor from todos, you instead go over all the todos and get the userIds
  • then you make a query in the user docs to get all docs from that array of userIds (should be a Mongo query operator to find any doc with an Id in a certain array)
  • in your query, make sure you only project/select relevant fields (eg userId, userName), this is in the options of a Meteor Collection query IIRC
  • return this cursor
  • subscribe to that publication from the client side
  • look up userIds to find usernames and display them

With methods (needs some tweaks for your use case)

  • Use array functions to map over each doc and get its ID, making a list of IDs. Send this list of IDs to a Meteor Method.
  • Check the current User’s ID and figure out if they’re allowed to see all the usernames they’re about to be sent (if the answer is always yes, just skip this part).
  • Get all the user docs for all the userIds provided (there’s a MongoDB query operator for finding all documents who have a field found in a specified array).
  • return a map (plain Js object in this case) of userIds to usernames
  • store that on the client state (in Svelte you can do a variable assignment but better off would be a store which persists between component lifetimes so it can act like a cache, cf SWR from Vercel)

Welcome Mireille,
There are many ways to achieve this - it depends on how reactive you want it to be. You can do something like this to do a join in your publication but it won’t update if the username changes. If you want more reactivity you could use publish composite

Meteor.publish("todos", function() {
    let subHandle = Todos.find({}).observeChanges({
        added: (id, entry) => {
            const user = Meteor.users.findOne({_id: entry.userId}, {fields: {profile: 1}});
            entry.username = user?.profile?.username || "Deleted User";
            this.added("todos", id, entry);
        },
        changed: (id, entry) => {
            //do same here 
            this.changed("todos", id, entry);
        },
        removed: id => {
            this.removed("todos", id);
        }
    });
    this.onStop(() => {
        subHandle.stop();
    });
});

I believe newTodo is supposed to be of type Object.

I see, I’ll stick with the observeChange or transform way of achieving it and I’ll cache the usernames in a Map or a KV storage.

Thanks for the help guys :slight_smile: