New Forest Admin agent Node.js - use transaction for PostgresSql

Hello :wave:

I would like to know how to use the transaction for a database with Postgres please?

Because in the old version you could do it like this :

const transaction = await myCovers.sequelize.transaction();

But on the new version of agent forest I couldn’t find any information about it

Context

  • Project name: Nostrum Care v3
  • Team name: Op
  • Environment name: Local
  • Agent (forest package) name & version:
    “@forestadmin/agent”: “^1.43.0”,
    “@forestadmin/datasource-sequelize”: “^1.5.21”,
    “@forestadmin/datasource-sql”: “^1.0.0”,
    “dotenv”: “^16.0.1”,
    “pg”: “^8.8.0”,
    “sequelize”: “^6.33.0”,
    “stripe”: “^14.17.0”
  • Database type: Postgresql

Thank you in advance for your help !!

Hello @jacques_liao

What do you want to achieve exactly ?

You want to use a transaction inside a smart action ?

Nicolas

hello @nbouliol

that’s right

When using the sequelize datasource, you can access sequelize in your action using context.collection.nativeDriver.sequelize

For example

.addAction("Action with transaction", {
  scope: "Single",
  execute: async (context, resultBuilder) => {
    const transaction = await context.collection.nativeDriver.sequelize.transaction();
  },
});
2 Likes

Thanks @nbouliol , I’ll test it right away!

And one last question, sorry because it has nothing to do with what is asked in the ticket, but how do you retrieve data from another table in a smart action?

Because in the doc we have examples of how to retrieve data from the table we’re working on and modify, but we don’t have any examples of how to retrieve data from another table that has no relationship with our current table.

You can retrieve data from other collections using the Forest Admin Query Interface

You can find the relevant documentation here

I try with

console.log('--------------------------------------------------------')
const refundsList = context.dataSource.getCollection('NP_refunds', { where: { fk_refundRequest: refundRequest.id } });
console.log(refundsList)
console.log('--------------------------------------------------------')

and I get

But how use this data ?

Because I want to retrieve data from another table via findAll where an Id.

Edit:
I have use this

const refundsList = await context.dataSource.getCollection('NP_refunds').aggregate({
    "conditionTree": { "field": "fk_refundRequest", "operator": "Equal", "value": refundRequest.id },
}, {});;

but I have this error

TypeError: Cannot read properties of undefined (reading 'type')

You should use the list method on the collection

/** List records matching filter */
list(filter: PaginatedFilter, projection: Projection): Promise<RecordData>;

You can find the filters documentation here

And here is an example snippet

      .addAction("Test get record", {
        scope: "Single",
        execute: async (context, resultBuilder) => {
          const recordId = await context.getRecordId();
          const data = await context.dataSource.getCollection("buns") // your collection, NP_refunds
            .list(
            {
              conditionTree: {
                field: "id",
                operator: "Equal",
                value: recordId,
              },
            },
            ["id"], // specify the fields you want to retrieve
          );
          console.log(data);
        },
      });

1 Like

Hello @nbouliol

Thanks, its work for me !!!

but how to use transaction in update ?

I test this but doesn’t work for me:

await context.dataSource.getCollection('NP_myCovers').update(
    {
        conditionTree: {
             field: "id",
             operator: "Equal",
             value: myCover.fk_myCover.id,
        },
    },
    myCoverData,
    transaction
);

If you want to use a sequelize transaction to update records, you shouldn’t use ForestAdmin queries but use sequelize update directly

Here is an example that should do want you want

          const recordId = await context.getRecordId();
          const sequelize = context.collection.nativeDriver.sequelize;
          const transaction = await sequelize.transaction();

          await sequelize.models.NP_myCovers.update(myCoverData, {
            where: { id: myCover.fk_myCover.id },
            transaction,
          });

          await transaction.commit();

I used this

 await sequelize.models.NP_myCovers.update(myCoverData, {
                where: {
                  id: myCover.fk_myCover.id
                }, transaction
              });

But I have this error in my logs:

TypeError: Cannot read properties of undefined (reading 'update')

I have set the variables correctly:

const sequelize = context.collection.nativeDriver.sequelize;
const transaction = await sequelize.transaction();

Maybe the models is not defined exactly like this.

You can inspect the models by logging sequelize.models

1 Like

Thank you yes indeed the table names were different in the models!

however, I encounter a case where I do not understand too much.

Each data that I send to the code below are sets.

await sequelize.models.refunds.create({
         id: uuid.v4(),
         amount: refundAmount,
         comment: agentComment,
         bic: refundRequest.fk_beneficiary.bic,
         iban: refundRequest.fk_beneficiary.iban,
         ibanHolderName: refundRequest.fk_beneficiary.ibanHolderName,
         status: status,
         fk_myCover: myCover.fk_myCover.id,
         fk_refundRequest: refundRequest.id,
         fk_userId: refundRequest.fk_user.id,
         fk_beneficiaryId: refundRequest.fk_beneficiary.id
}, { transaction });

But when I look at forest I don’t see the set ids

The fk_myCover and fk_refundRequest fields are relationships to other tables, is this what’s causing the problem? If so, how do you set a relationship field?

Do you mean it’s a forest smart relationship ? Or a relationship in your database ?

From which collection are you running the action ? And what kind of record does it create ?

hello @nbouliol

Is a relationship in our database.
It’s in the smart action that I’m trying to do this, and try to add new info to the Refunds table.
For the last question, I don’t really understand.

Hello

Can you please share the action code ? So it will help me understand what’s wrong

No problem

      collection.addAction('Traiter la demande de remboursement', {
        scope: 'Single',
        form: [
          {
            label: 'Traiter la demande',
            type: 'Enum',
            enumValues: ['Valider', 'Refuser', 'A modifier'],
            isRequired: true,
          },
          {
            label: 'montant',
            type: 'Number',
            value: async context => {
              const refundRequestData = await context.getRecord(['amount']);
              const refundStatus = await context.formValues['Traiter la demande'];
              if (refundStatus === 'Valider') {
                return refundRequestData.amount;
              }
              return 0;
            },
            isRequired: true
          },
          {
            label: 'commentaire',
            type: 'String',
            widget: 'TextArea',
            placeholder: "Commentaire visible au client, attention en cas de suppression de l'ancien message ce dernier sera perdu. Privilégiez un saut à la ligne si vous souhaiter compléter le message.",
            defaultValue: async context => {
              const refundRequestData = await context.getRecord(['agentComment']);
              if (refundRequestData.agentComment) {
                return refundRequestData.agentComment;
              }
              return null;
            },
            isRequired: async context => {
              const refundStatus = context.formValues['Traiter la demande'];
              if (refundStatus === 'Refuser') {
                return true;
              }
              return false;
            }
          },
        ],
        execute: async (context, resultBuilder) => {
          const sequelize = context.collection.nativeDriver.sequelize;
          const transaction = await sequelize.transaction();
          try {
            // Get the values of the input fields entered by the admin user.
            const attrs = context.formValues;
            const refundAmount = attrs.montant;
            const agentComment = attrs.commentaire;
            const refundRequestFinalStatus = attrs['Traiter la demande'];
            // Retrieve actual refund request beneficiary_firstname, user_email, invoiceAmount amount, fk_beneficiary.id, fk_user.id, fk_beneficiary.iban, fk_beneficiary.bic & fk_beneficiary.ibanHolderName
            const refundRequest = await context.getRecord(['id', 'beneficiary_firstname', 'user_email', 'invoiceAmount', 'amount', 'fk_user:id', 'fk_beneficiary:id', 'fk_beneficiary:iban', 'fk_beneficiary:bic', 'fk_beneficiary:ibanHolderName']);
            // Check refund compliance 
            if (attrs.montant < 0) {
              console.error(`Can not process refund request ${refundRequest.id}, the amount input can not be inferior to 0`);
              return resultBuilder.error('La demande ne peut être traitée, le montant que vous renseignez ne doit pas être négatif');
            }
            // Retrieve beneficiary cover fk_myCover.id, fk_myCover.fk_cover.title, fk_myCover.refundLimit, fk_myCover.fk_myCommonPot, fk_myCover.refundedBalanceEstimation, fk_myCover.refunded, fk_myCover.refundedQuantityEstimation, fk_myCover.refundedQuantity
            const myCover = await context.getRecord(['fk_myCover:id', 'fk_myCover:fk_cover:title', 'fk_myCover:refundLimit', 'fk_myCover:fk_myCommonPot:id', 'fk_myCover:refundedBalanceEstimation', 'fk_myCover:refunded', 'fk_myCover:refundedQuantityEstimation', 'fk_myCover:refundedQuantity']);
            const sendEmailData = {
              userEmail: refundRequest.user_email,
              beneficiaryFirstname: refundRequest.beneficiary_firstname,
              receiptAmount: refundRequest.invoiceAmount,
              refundAmount: refundRequest.amount,
              guarantee: myCover.fk_myCover.fk_cover.title,
              refundRequestDate: moment().format('DD/MM/YYYY'),
              refundLimit: myCover.fk_myCover.refundLimit,
            }
            let message = {};
            let pushContent = '';
            let status = null;
            switch (refundRequestFinalStatus) {
              case 'Valider':
                status = "VALIDATED";
                message = 'La demande a été validée';
                break;
              case 'A modifier':
                status = "EDITABLE";
                message = 'La demande peut être modifiée par l\'utilisateur'
                break;
              default:
                status = "REFUSED";
                message = 'La demande a été refusée'
                break;
            }
            // Process refund request 
            try {
              // retrieve refunds attached to refund request 
              const refundsList = await context.dataSource.getCollection('NP_refunds').list(
                {
                  conditionTree: {
                    field: "id",
                    operator: "Equal",
                    value: refundRequest.id,
                  },
                },
                ["id", "amount", "status"],
              );
              let refundedAmount = 0;
              for (const refund of refundsList) {
                // set refund request refunded amount (sum of all refunds)
                if (refund.status !== 'CLOSED') {
                  refundedAmount += refund.amount;
                }
              }
              let myCommonPot, myCoverData, myCommonPotData = null;
              // retrieve cover's attached common pot if exists 
              if (myCover.fk_myCover?.fk_myCommonPot?.id) {
                // myCommonPot = await myCommonPots.findByPk(myCover.fkMyCommonPotIdKey);
                myCommonPot = await context.dataSource.getCollection('NP_myCommonPots').list(
                  {
                    conditionTree: {
                      field: "id",
                      operator: "Equal",
                      value: myCover.fk_myCover.fk_myCommonPot.id,
                    },
                  },
                  ["id", "refunded", "refundedBalanceEstimation", "refundedBalanceEstimation"],
                );
              };
              // process refund request
              if (status === 'VALIDATED') {
                // set cover + common pot consumption + estimation
                myCoverData = {
                  refunded: myCover.fk_myCover.refunded + refundAmount,
                  refundedQuantity: myCover.fk_myCover.refundedQuantity + 1
                };
                myCoverData.refundedBalanceEstimation = myCover.fk_myCover.refundedBalanceEstimation - refundRequest.amount + refundAmount;
                if (myCommonPot) {
                  myCommonPotData = { refunded: myCommonPot[0].refunded + refundAmount };
                  myCommonPotData.refundedBalanceEstimation = myCommonPot[0].refundedBalanceEstimation - refundRequest.amount + refundAmount;
                }
                refundedAmount = refundAmount;
                // add allowed amount to refunded amount
                const currentRefundLimit = myCover.fk_myCover.refundLimit - myCover.fk_myCover.refunded - refundAmount;
                const previousRefundLimit = myCover.fk_myCover.refundLimit - myCover.fk_myCover.refunded;
                // Send email to beneficiary  
                // await mailjetAPI.sendConfirmationRefundRequest(sendEmailData.userEmail, sendEmailData.beneficiaryFirstname, sendEmailData.receiptAmount, sendEmailData.refundAmount, sendEmailData.guarantee, sendEmailData.refundRequestDate, currentRefundLimit, previousRefundLimit);
              } else {
                // rollback cover & common pot balance + quantity estimation
                let refundedQuantityEstimation;
                if (!myCover.fk_myCover.refundedQuantityEstimation || myCover.fk_myCover.refundedQuantityEstimation <= 0) {
                  refundedQuantityEstimation = 0;
                } else {
                  refundedQuantityEstimation = myCover.fk_myCover.refundedQuantityEstimation - 1;
                }
                myCoverData = {
                  refundedBalanceEstimation: myCover.fk_myCover.refundedBalanceEstimation - refundRequest.amount,
                  refundedQuantityEstimation
                };
                if (myCommonPot) {
                  myCommonPotData = {
                    refundedBalanceEstimation: myCommonPot[0].refundedBalanceEstimation - refundRequest.amount
                  };
                }
                if (status === 'REFUSED') {
                  refundedAmount = 0;
                  // Send email to beneficiary  
                  // await mailjetAPI.sendRefusedRefundRequest(sendEmailData.userEmail, sendEmailData.beneficiaryFirstname, sendEmailData.receiptAmount, sendEmailData.refundAmount, sendEmailData.guarantee);
                } else {
                  refundedAmount = 0;
                  // Send email to beneficiary  
                  // await mailjetAPI.sendEditableRefundRequest(sendEmailData.userEmail, sendEmailData.beneficiaryFirstname, sendEmailData.receiptAmount, sendEmailData.refundAmount, sendEmailData.guarantee);
                }
              }
              // if everything ok, update cover 
              await sequelize.models.covers.update(myCoverData, {
                where: {
                  id: myCover.fk_myCover.id
                }, transaction
              });
              // if cover belongs to common pot, update common pot  
              if (myCommonPot) {
                await sequelize.models.myCommonPots.update(myCommonPotData, { where: { id: myCommonPot[0].id }, transaction });
              }
              // create refund if refund request is validated
              if (status === 'VALIDATED') {
                await sequelize.models.refunds.create({
                  id: uuid.v4(),
                  amount: refundAmount,
                  comment: agentComment,
                  bic: refundRequest.fk_beneficiary.bic,
                  iban: refundRequest.fk_beneficiary.iban,
                  ibanHolderName: refundRequest.fk_beneficiary.ibanHolderName,
                  status: status,
                  fk_myCover: myCover.fk_myCover.id,
                  fk_refundRequest: refundRequest.id,
                  fk_userId: refundRequest.fk_user.id,
                  fk_beneficiaryId: refundRequest.fk_beneficiary.id
                }, { transaction });
              }
              // update refund request 
              await sequelize.models.refundRequests.update({
                status,
                processingDateNostrumCare: new Date(),
                refundedAmount,
                agentComment
              }, { where: { id: refundRequest.id }, transaction });
              // Commit transaction if everything's ok
              await transaction.commit();
            } catch (error) {
              console.error(`Error processing refund request (${refundRequest.id}), error : ${error}`);
              await transaction.rollback();
              return resultBuilder.success(`Problème survenue lors de la procédure, veuillez contacter un developpeur backend`);
            }
            return resultBuilder.success(message);
          } catch (error) {
            console.error(`[POST /actions/traiter-la-demande-de-remboursement] : error : ${error}`);
            await transaction.rollback();
            return resultBuilder.success(`Problème survenue lors de la procédure, veuillez contacter un developpeur backend`);
          }
        },
      });

then everything’s ok it’s just that the ids for fk_refundRequest and fk_myCover aren’t added in the create, yet I have the ids when I log them

Thank you for sharing your code.

Are the columns fk_myCover and fk_refundRequest set in the database for the entries created from the action ?

On Forest side, do you see the relationships values after refreshing the page ?

Small tip on your smart action code, in your catch when the actions fails, you can use resultBuilder.error to display an error notification (doc)

No, the fields are there for better tracking, but they weren’t created for smart action.

No, I can’t see anything after refreshing.

Yes it’s true thank you !

If the values are good in the database, can you please open a new ticket for your issue with the relationships not displayed since it is an other issue than the initial thread.