Newsletter

Stay Informed!

Subscribe to our newsletter and receive news, advice, and more on e-commerce.

Technology

Transactions with Strapi v4

We have used Strapi in two different types of projects so far. One as a CMS and the other we are using Strapi as a backend for a Next.JS application.

In the latter we use transactions for handling the changes of the data during an import process.

If the concept of transactions is unfamiliar to you, consider it as a means to create a reference point in time. This reference point allows you to revert any changes made to the database if something goes wrong during the process. Here is a definition of transactions from the PostgreSQL:

"Transactions are a fundamental concept of all database systems. The essential point of a transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, and if some failure occurs that prevents the transaction from completing, then none of the steps affect the database at all."

Source: https://www.postgresql.org/docs/8.3/tutorial-transactions.html

This article will show you how we use transactions in Strapi v4 and how to create unit tests for those transactions. But first of all, let’s explain why we needed to use transactions.

Reason for transactions

What is the reason for using transactions

In our project where we use Strapi as a backend, we have to import a lot of data at once (for example: thousands of lines in a csv file). This data is read and the process will either create or update the rows in the database, which will appear as entities in the Strapi admin panel. The frontend will then query these entities through the GraphQL plugin or API endpoints.

During the import process, it is essential to avoid overwriting the production database directly. This is because any issues that occur during the import can result in a database that has some data modified, while other data may not have been inserted or updated. This will result in having corrupted data, and the users will see incorrect data (not fully updated).

There are a few solutions to this problem but none are officially documented as of today and we decided to go with transactions.
Let’s now see how we use transactions with Strapi v4.

Transaction use

How we use transactions

In the background, Strapi uses Knex.js, a SQL query builder for many types of databases (we are using PostgreSQL in this example) and it supports transactions.

Let’s create a very simple content type for this example. It will be called product and will have two properties (name and price):

Now to the transactions!
We can get the Knex object directly from the global Strapi instance:

 

/* global strapi */

const knexObject = strapi.db.connection

 

And we can create a transaction using this method:

 

/* global strapi */

const newTransaction = strapi.db.connection.transaction()

 

With this newTransaction constant, you can perform any changes to the database and be able to commit or rollback at any point.

Here is how to insert a new product using this method:

 

/* global strapi */

const newTransaction = strapi.db.connection.transaction()
newTransaction('products').insert({
  name: 'My new product',
  price: 3.50,
}, '*')

 

The new product is now part of the newTransaction, it is not yet present in the database. We now have the choice to commit (apply the changes) or rollback (go back to the point in time/state before the transaction was started).

Here is how to commit the transaction:

 

newTransaction.commit()

 

Here is how to rollback the transaction:

 

newTransaction.rollback()
Testing

Testing

Let's now test the two cases and find the entity with Strapi:

Commit

 

/* global strapi */

const data = {
  name: 'My new Product',
  price: 3.5,
}

it('Inserts a product with a transaction', async () => {
  const newTransaction = await strapi.db.connection.transaction()
  const insertedRows = await newTransaction('products').insert(data, '*')

  expect(insertedRows).toHaveLength(1)

  // Find it with Strapi - still not present in the database
  const productResult = await strapi.entityService.findMany(
    'api::product.product',
    {
      filters: data,
    }
  )

  expect(productResult).toHaveLength(0)

  await newTransaction.commit()

  // Find it with Strapi - found it!
  const productResult = await strapi.entityService.findMany(
    'api::product.product',
    {
      filters: data,
    }
  )

  expect(productResult).toHaveLength(1)

  const newProduct = productResult[0]
  expect(newProduct).toMatchObject(data)

  await strapi.entityService.delete('api::product.product', newProduct.id)
})

In this test you can see that we first create a transaction, then we insert a new row in the transaction. If we search for the entity with Strapi before commiting, we do not find it. Then we commit the transaction and we can search for our new row with Strapi's entity service and sure enough we find it.

Rollback

 

/* global strapi */

const data = {
  name: 'My new Product',
  price: 3.5,
}

it('Rollbacks a transaction', async () => {
  const newTransaction = await strapi.db.connection.transaction()
  const insertedRows = await newTransaction('products').insert(data, '*')

  expect(insertedRows).toHaveLength(1)
  await newTransaction.rollback()

  // Make sure the data is not inserted with Strapi
  const productResult = await strapi.entityService.findMany(
    'api::product.product',
    {
      filters: data,
    }
  )

  expect(productResult).toHaveLength(0)
})

Here we can see that the insertedRows have a length of one before the transaction rollback. Once we decide to not apply the changes, Strapi will not find the new row, which is exactly what we expect.

Conclusion

Conclusion

In this article we saw how to use transactions in Strapi v4 in order to either commit new data or rollback the whole transaction. And both tests show us how they are used and that transactions with Knex work.
Of course you can do more than inserting, Knex has good documentation on how to update, delete, work with relations, etc.
This is a great solution to keep our data uncorrupted and make sure that we don't disturb the users while manipulating data in the background. In a future article we will explain how we added Typescript support to transactions. The concept will stay the same and it will allow you to have even more control over the data.

Author: Geoffroy Baumier, Senior Software Engineer & Team Lead

Your Con­tact at UFirst

Portrait Jor­dán Ja­rolím
Partner & Tech. Architect

Jor­dán Ja­rolím

Start your digital future with us.
We look forward to it!