Header illustration for Syncing Relationships in AdonisJS

Syncing Relationships in AdonisJS

Syncing of nested relations

Introduction

AdonisJs is a Node.js web framework that makes it easy to build web applications and APIs. One of the key features of AdonisJs is its object-relational mapping (ORM), which allows you to work with databases using object-oriented code instead of SQL queries. AdonisJs 5 introduced several improvements to the ORM, including better TypeScript support, a more intuitive API, and improved performance.

One of the common tasks when working with databases is syncing related data. For example, if you have a user model with many posts, and you want to update the user's posts based on new data, you need to sync the new data with the existing posts. AdonisJs provides several ways to work with related data, including eager loading, lazy loading, and preloading. However, syncing related data can still be a challenge, especially when dealing with nested relations.

In this blog post, we'll look at how to create a syncRelation function for AdonisJs 5 that makes it easy to sync related data, even with nested relations.

The syncRelation function

The syncRelation function is a utility function that allows you to sync related data with a related model's data. It takes four parameters:

relation: The relation to sync. This should be a QueryBuilder instance that represents the relation between two models.
data: The data to sync with the relation. This should be an array of ModelObject instances that represent the data to sync.
cb: The callback function to use to recursively sync related data. This function takes two parameters: the data to sync and the related model to sync it with. The callback function should return a Promise that resolves when the related data has been synced.
allowDelete: Whether or not to allow deleting related data that is not in the given data. This parameter is optional and defaults to false.

Here's the TypeScript signature of the syncRelation function:

async function syncRelation<T extends LucidModel, U extends LucidModel>(<br />
  relation: QueryBuilder<T, U>,<br />
  data: ModelObject<U>[],<br />
  cb: (data: ModelObject<U>, model: U) => Promise<void>,<br />
  allowDelete = false<br />
): Promise<void>
                

Let's look at each parameter in more detail.

The relation parameter

The relation parameter is a QueryBuilder instance that represents the relation between two models. This can be any type of relation, including hasOne, hasMany, belongsTo, and belongsToMany. The QueryBuilder instance allows you to fetch, create, update, and delete related data.

The data parameter

The data parameter is an array of ModelObject instances that represent the data to sync with the relation. Each ModelObject instance should have a property that corresponds to the primary key of the related model. For example, if you're syncing posts with a user, each post ModelObject should have an id property that corresponds to the id of the related user.

The cb parameter

The cb parameter is a callback function that is used to recursively sync related data. This function takes two parameters: the data to sync and the related model to sync it with. The data parameter is a ModelObject instance that represents the data to sync, and the model parameter is a related model instance that represents the data in the database. The cb function should modify the model instance to reflect the data in the data instance. It can also call the syncRelation function again to sync any nested relations. The cb function should return a Promise that resolves when the related data has been synced.

The allowDelete parameter

The allowDelete parameter is an optional boolean parameter that specifies whether or not to allow deleting related data that is not in the given data. If set to true, the syncRelation function will delete any related models that are not in the data parameter. This can be useful when you want to delete any related data that has been removed from the source data.

Example usage

Let's look at an example usage of the syncRelation function. Suppose we have a blog application with a User model, a Post model, and a Comment model. Each user has many posts, and each post has many comments. We want to update a user's posts and comments based on new data that we receive from an API. Here's an example implementation of the syncRelation function for this use case:

import Database from '@ioc:Adonis/Lucid/Database'
import { LucidModel, ModelObject, QueryBuilder } from '@ioc:Adonis/Lucid/Orm'

async function syncRelation<T extends LucidModel, U extends LucidModel>(
  relation: QueryBuilder<T, U>,
  data: ModelObject<U>[],
  cb: (data: ModelObject<U>, model: U) => Promise<void>,
  allowDelete = false
): Promise<void> {
  await Database.transaction(async (trx) => {
    const existingData = await relation.transacting(trx).fetch()

    for (const existingModel of existingData.rows) {
      const newData = data.find((d) => d.id === existingModel.id)
      if (!newData && allowDelete) {
        await existingModel.delete()
      } else if (newData) {
        await cb(newData, existingModel)
      }
    }

    for (const newData of data) {
      const existingModel = existingData.rows.find((m) => m.id === newData.id)
      if (!existingModel) {
        await relation.transacting(trx).create(newData)
      }
    }
  })
}
This implementation uses the Database.transaction method to ensure that all updates are done atomically. It fetches the existing data from the database, iterates over it, and updates or deletes any related models as necessary. It then iterates over the new data and creates any missing models.

Here's how we can use the syncRelation function to sync a user's posts and comments:

import User from 'App/Models/User'

const user = await User.find(1)
const data = {
  posts: [
    {
      id: 1,
      title: 'New title',
      body: 'New body',
      comments: [
        {
          id: 1,
          body: 'New comment',
          user: {
            id: 1,
            name: 'New name',
            email: 'newemail@example.com',
          },
        },
      ],
    },
  ],
}

await syncRelation(user.posts(), data.posts, async (postData, postModel) => {
  postModel.title = postData.title
  postModel.body = postData.body

  await syncRelation(postModel.comments(), postData.comments, async (commentData, commentModel) => {
    commentModel.body = commentData.body

    await syncRelation(commentModel.user(), commentData.user, async (userData, userModel) => {
      userModel.name = userData.name
      userModel.email = userData.email
    })
  })
}, true)
                

In this example, we're syncing a user's posts and comments based on the data object we received. The data object has a posts array, each element of which has a comments array, each element of which has a user object. We use the syncRelation function to sync each level of the nested relations.

When we call syncRelation(user.posts(), data.posts, ...), the cb function is called for each post that already exists in the database. We update the post's title and body based on the data object. We then call syncRelation(postModel.comments(), postData.comments, ...) to sync the comments for the post.

The cb function for the comments updates the comment's body, and then calls syncRelation(commentModel.user(), commentData.user, ...) to sync the user for the comment. Finally, the cb function for the user updates the user's name and email.

If any related data has been removed from the data object (e.g. a comment has been deleted), the allowDelete parameter allows us to delete the related model from the database.

Conclusion

In this article, we've looked at how to implement a syncRelation function in AdonisJS 5 to sync related data based on a given data object. The syncRelation function can be used to update or create related models, and it can handle nested relations as well.

Using the syncRelation function can make it easier to keep related data in sync when updating models based on incoming data. By handling related data in a consistent and reliable way, we can avoid bugs and ensure that our application's data remains consistent.

Creating, updating and maintaining PDF's in any codebase can be a pain. You have to make sure all the packages stay up to date, eventually you run in to some limitations of any library that leaves you unable to design the document exactly how you want it

This is why we created PDFdesignAPI.com, an easy visual document designer connected to an API. Design your perfect document, send the data to the API and get a beautifull PDF in return.

Want to know more? Try it today for free.

Start for free.

Dont wait any longer, start your free trial today and start creating your own documents.