Skip to main content

Relationships between entities

In the previous section, we created todo entity.

In this section we'll create list entity and create relationships between todo and list:

  • todo.list will create a reference to the corresponding list entity
  • list.todos will return an array of todos belonging to the given list

Ok, let's start by defining list entity:

interface List {
id: string;
name: string;
}

const listEntity = defineEntity<List>({
name: "list",
fields: ["id", "name"],
});

Let's add a listId field to the todo entity we created before.

import { defineEntity } from "@clientdb/core";

interface Todo {
id: string;
title: string;
doneAt: Date;
listId: string;
}

const todoEntity = defineEntity<Todo>({
name: "todo",
fields: ["id", "title", "doneAt", "listId"],
});

Great! Our entities now have all the data we need, but we still don't have relationships between them.

To do that, we'll create the so-called view on both our entities' definitions.

The view allows us to append custom-derived data to our entities.

Add todo.list relation

We'll call .addView on our todo entity definition. This will add custom data to every todo.

import { defineEntity } from "@clientdb/core";

const todoEntity = defineEntity<Todo>({
// ...
}).addView((todo, { db }) => {
return {
get list() {
return db.entity(listEntity).findById(todo.listId);
},
};
});

The function we passed to addView is called with 2 arguments:

  • data of the entity (todo)
  • helper object which includes the db property. db is the database given todo belongs to. We can use it to find other entities (todos or lists) in the same database.

Using those, we can create a list property getter that will try to find a list with an id equal to todo.listId.

info

Data passed to addView (todo variable in our case) is observable. This means that if you change the todo.listId property, the list property will be updated.

caution

View properties should be getters. If we'd define view as:

return {
list: db.entity(listEntity).findById(todo.listId),
};

instead of

return {
get list() {
return db.entity(listEntity).findById(todo.listId);
},
};

List property would not be observable as relation will be resolved at the moment when we create entity instead of when we read todo.list property. Read more in mobx guide.

Adding list.todos property

Ok, now we can also add a relation to the list entity. It'll return an array of todos that are part of this list.

const listEntity = defineEntity<List>({
// ...
}).addView((list, { db }) => {
return {
get todos() {
return db.entity(todoEntity).query({
listId: list.id,
}).all;
},
};
});

Our relations are ready.

The last thing we need to do is update our database to be aware of our new listEntity entity

import { createClientDb } from "@clientdb/core";

const db = createClientDb([todoEntity, listEntity]);

We're ready to go.

Now let's populate our database with some todos and lists.

const list = db.add(listEntity, {
id: "list-1",
name: "Groceries",
});

const todo1 = db.add(todoEntity, {
id: "todo-1",
title: "Buy milk",
doneAt: null,
listId: list.id,
});

const todo2 = db.add(todoEntity, {
id: "todo-2",
title: "Buy books",
doneAt: null,
listId: list.id,
});

Let's read data using newly created relations:

list.todos; // [todo1, todo2]
todo1.list.name; // "Groceries"
todo1.list === todo2.list; // true

Recap

Full code so far would be:

import { createClientDb } from "@clientdb/core";
import { defineEntity } from "@clientdb/core";

interface Todo {
id: string;
title: string;
doneAt: Date;
listId: string;
}

const todoEntity = defineEntity<Todo>({
name: "todo",
fields: ["id", "title", "doneAt", "listId"],
}).addView((todo, { db }) => {
return {
get list() {
return db.entity(listEntity).findById(todo.listId);
},
};
});

interface List {
id: string;
name: string;
}

const listEntity = defineEntity<List>({
name: "list",
fields: ["id", "name"],
}).addView((list, { db }) => {
return {
get todos() {
return db.entity(todoEntity).query({
listId: list.id,
}).all;
},
};
});

const db = createClientDb([todoEntity, listEntity]);