Skip to content

Commit

Permalink
Merge pull request #124 from letehaha/feat/improve-sequelize-tx-usage…
Browse files Browse the repository at this point in the history
…-across-the-app

feat: Improve Sequelize transaction usage in the whole app
  • Loading branch information
letehaha authored Sep 13, 2024
2 parents 8077267 + f16103b commit cf1c749
Show file tree
Hide file tree
Showing 53 changed files with 1,874 additions and 2,738 deletions.
51 changes: 51 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"dependencies": {
"axios": "1.7.4",
"bcryptjs": "2.4.3",
"cls-hooked": "^4.2.2",
"config": "3.3.11",
"connect-redis": "5.0.0",
"cors": "2.8.5",
Expand Down
6 changes: 0 additions & 6 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Express from 'express';
import { ZodIssue, z } from 'zod';
import { Transaction } from 'sequelize/types';
import { API_RESPONSE_STATUS } from 'shared-types';
import Users from '@models/Users.model';

Expand All @@ -21,10 +20,5 @@ export interface CustomResponse extends Express.Response {
json: Send<this>;
}

export interface GenericSequelizeModelAttributes {
transaction?: Transaction;
raw?: boolean;
}

export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
export type UnwrapArray<T> = T extends (infer U)[] ? U : T;
219 changes: 97 additions & 122 deletions src/controllers/banks/monobank.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,15 @@ async function updateWebhookAxios({ userToken }: { userToken?: string } = {}) {
});
}

async function createMonoTransaction(
{
data,
account,
userId,
}: {
data: ExternalMonobankTransactionResponse;
account: AccountModel;
userId: number;
},
// attributes: GenericSequelizeModelAttributes = {},
) {
async function createMonoTransaction({
data,
account,
userId,
}: {
data: ExternalMonobankTransactionResponse;
account: AccountModel;
userId: number;
}) {
const isTransactionExists = await transactionsService.getTransactionBySomeId({
originalId: data.id,
userId,
Expand Down Expand Up @@ -465,89 +462,77 @@ export const loadTransactions = async (req, res: CustomResponse) => {
export const refreshAccounts = async (req, res) => {
const { id: systemUserId } = req.user;

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

try {
const monoUser = await monobankUsersService.getUserBySystemId(
{ systemUserId },
{ transaction },
);
return connection.sequelize.transaction(async () => {
try {
const monoUser = await monobankUsersService.getUserBySystemId({ systemUserId });

if (!monoUser) {
await transaction.commit();

return res.status(ERROR_CODES.NotFoundError).json({
status: API_RESPONSE_STATUS.error,
response: {
message: 'Current user does not have any paired monobank user.',
code: API_ERROR_CODES.monobankUserNotPaired,
},
});
}

const token = `monobank-${systemUserId}-client-info`;
const tempToken = await req.redisClient.get(token);

if (!tempToken) {
let clientInfo: ExternalMonobankClientInfoResponse;
try {
clientInfo = (
await axios({
method: 'GET',
url: `${hostname}/personal/client-info`,
responseType: 'json',
headers: {
'X-Token': monoUser.apiToken,
},
})
).data;
} catch (e) {
await transaction.rollback();

if (e.response) {
if (e.response.data.errorDescription === "Unknown 'X-Token'") {
// Set user token to empty, since it is already invalid. In that way
// we can let BE/FE know that last token was invalid and now it
// needs to be updated
await monobankUsersService.updateUser({ systemUserId, apiToken: '' }, { transaction });

return res.status(403).json({
status: API_RESPONSE_STATUS.error,
response: {
code: API_ERROR_CODES.monobankTokenInvalid,
message: "User's token is invalid!",
},
});
}
}
return res.status(500).json({
if (!monoUser) {
return res.status(ERROR_CODES.NotFoundError).json({
status: API_RESPONSE_STATUS.error,
response: {
message: 'Something bad happened while trying to contact Monobank!',
message: 'Current user does not have any paired monobank user.',
code: API_ERROR_CODES.monobankUserNotPaired,
},
});
}

await req.redisClient.set(token, true);
await req.redisClient.expire(token, 60);
const token = `monobank-${systemUserId}-client-info`;
const tempToken = await req.redisClient.get(token);

if (!tempToken) {
let clientInfo: ExternalMonobankClientInfoResponse;
try {
clientInfo = (
await axios({
method: 'GET',
url: `${hostname}/personal/client-info`,
responseType: 'json',
headers: {
'X-Token': monoUser.apiToken,
},
})
).data;
} catch (e) {
if (e.response) {
if (e.response.data.errorDescription === "Unknown 'X-Token'") {
// Set user token to empty, since it is already invalid. In that way
// we can let BE/FE know that last token was invalid and now it
// needs to be updated
await monobankUsersService.updateUser({ systemUserId, apiToken: '' });

return res.status(403).json({
status: API_RESPONSE_STATUS.error,
response: {
code: API_ERROR_CODES.monobankTokenInvalid,
message: "User's token is invalid!",
},
});
}
}
return res.status(500).json({
status: API_RESPONSE_STATUS.error,
response: {
message: 'Something bad happened while trying to contact Monobank!',
},
});
}

await req.redisClient.set(token, true);
await req.redisClient.expire(token, 60);

const existingAccounts = await accountsService.getAccountsByExternalIds(
{
const existingAccounts = await accountsService.getAccountsByExternalIds({
userId: monoUser.systemUserId,
externalIds: clientInfo.accounts.map((item) => item.id),
},
{ transaction, raw: true },
);
});

const accountsToUpdate = [];
const accountsToCreate = [];
clientInfo.accounts.forEach((account) => {
const existingAccount = existingAccounts.find((acc) => acc.externalId === account.id);
const accountsToUpdate = [];
const accountsToCreate = [];
clientInfo.accounts.forEach((account) => {
const existingAccount = existingAccounts.find((acc) => acc.externalId === account.id);

if (existingAccount) {
accountsToUpdate.push(
accountsService.updateAccount(
{
if (existingAccount) {
accountsToUpdate.push(
accountsService.updateAccount({
id: existingAccount.id,
currentBalance: account.balance,
creditLimit: account.creditLimit,
Expand All @@ -557,51 +542,41 @@ export const refreshAccounts = async (req, res) => {
// cashbackType: item.cashbackType,
// type: item.type,
// iban: item.iban,
},
{ transaction },
),
);
} else {
accountsToCreate.push(
accountsService.createSystemAccountsFromMonobankAccounts({
userId: systemUserId,
monoAccounts: [account],
}),
);
}
});
}),
);
} else {
accountsToCreate.push(
accountsService.createSystemAccountsFromMonobankAccounts({
userId: systemUserId,
monoAccounts: [account],
}),
);
}
});

await Promise.all(accountsToUpdate);
await Promise.all(accountsToCreate);
await Promise.all(accountsToUpdate);
await Promise.all(accountsToCreate);

const accounts = await accountsService.getAccounts(
{
const accounts = await accountsService.getAccounts({
userId: monoUser.systemUserId,
type: ACCOUNT_TYPES.monobank,
},
{ transaction },
);
});

await transaction.commit();
return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: accounts,
});
}

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: accounts,
return res.status(ERROR_CODES.TooManyRequests).json({
status: API_RESPONSE_STATUS.error,
response: {
code: API_ERROR_CODES.tooManyRequests,
message: 'Too many requests! Request cannot be called more that once a minute!',
},
});
} catch (err) {
errorHandler(res, err);
}

await transaction.commit();

return res.status(ERROR_CODES.TooManyRequests).json({
status: API_RESPONSE_STATUS.error,
response: {
code: API_ERROR_CODES.tooManyRequests,
message: 'Too many requests! Request cannot be called more that once a minute!',
},
});
} catch (err) {
await transaction.rollback();

errorHandler(res, err);
}
});
};
Loading

0 comments on commit cf1c749

Please sign in to comment.