Before you jump into this content, you need to have a basic understanding of the following:
Within our company UFirst Group, we have been using Strapi in two different projects. In one as a CMS and in 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.
For those who are unfamiliar with the concept of transactions, think of it as creating a point in time that you can go back to if something goes wrong when doing any changes to the database. 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.
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.
When doing the import we don’t want to overwrite the production database directly because if something goes wrong during the import process, we will have some data modified in the database and some data that has not 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.
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:
1/* global strapi */
2
3const knexObject = strapi.db.connection;
And we can create a transaction using this method:
1/* global strapi */
2
3const 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:
1/* global strapi */
2
3const newTransaction = strapi.db.connection.transaction();
4newTransaction("products").insert(
5 {
6 name: "My new product",
7 price: 3.5,
8 },
9 "*"
10);
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:
1newTransaction.commit();
Here is how to rollback the transaction:
1newTransaction.rollback();
Let’s now test the two cases and find the entity with Strapi:
1/* global strapi */
2
3const data = {
4 name: "My new Product",
5 price: 3.5,
6};
7
8it("Inserts a product with a transaction", async () => {
9 const newTransaction = await strapi.db.connection.transaction();
10 const insertedRows = await newTransaction("products").insert(data, "*");
11
12 expect(insertedRows).toHaveLength(1);
13
14 // Find it with Strapi - still not present in the database
15 const productResult = await strapi.entityService.findMany(
16 "api::product.product",
17 {
18 filters: data,
19 }
20 );
21
22 expect(productResult).toHaveLength(0);
23
24 await newTransaction.commit();
25
26 // Find it with Strapi - found it!
27 const productResultAfterCommit = await strapi.entityService.findMany(
28 "api::product.product",
29 {
30 filters: data,
31 }
32 );
33
34 expect(productResultAfterCommit).toHaveLength(1);
35
36 const newProduct = productResultAfterCommit[0];
37 expect(newProduct).toMatchObject(data);
38
39 await strapi.entityService.delete("api::product.product", newProduct.id);
40});
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 committing, 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.
1/* global strapi */
2
3const data = {
4 name: "My new Product",
5 price: 3.5,
6};
7
8it("Rollbacks a transaction", async () => {
9 const newTransaction = await strapi.db.connection.transaction();
10 const insertedRows = await newTransaction("products").insert(data, "*");
11
12 expect(insertedRows).toHaveLength(1);
13 await newTransaction.rollback();
14
15 // Make sure the data is not inserted with Strapi
16 const productResult = await strapi.entityService.findMany(
17 "api::product.product",
18 {
19 filters: data,
20 }
21 );
22
23 expect(productResult).toHaveLength(0);
24});
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.
Code Source on GitHub: https://github.com/ufirstgroup/strapi-transactions
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.
Senior Software Engineer & Team Lead at UFirst Group