Skip to content

Commit

Permalink
[TECH] Ajoute la domainTransaction dans le AsyncLocalStorage (PIX-13257
Browse files Browse the repository at this point in the history
…).

 #9423
  • Loading branch information
pix-service-auto-merge authored Jul 4, 2024
2 parents d81e9b3 + 726a519 commit 295f2b2
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 30 deletions.
3 changes: 2 additions & 1 deletion api/lib/infrastructure/repositories/campaign-repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ const getCampaignCodeByCampaignParticipationId = async function (campaignPartici
};

const getCampaignIdByCampaignParticipationId = async function (campaignParticipationId) {
const campaign = await knex('campaigns')
const knexConn = DomainTransaction.getConnection();
const campaign = await knexConn('campaigns')
.select('campaigns.id')
.join('campaign-participations', 'campaign-participations.campaignId', 'campaigns.id')
.where({ 'campaign-participations.id': campaignParticipationId })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,9 @@ const updateParticipantExternalId = async function (request, h) {
const deleteCampaignParticipationForAdmin = async function (request, h) {
const { userId } = request.auth.credentials;
const { id: campaignParticipationId } = request.params;
await DomainTransaction.execute(async (domainTransaction) => {
await usecases.deleteCampaignParticipationForAdmin({
userId,
campaignParticipationId,
domainTransaction,
});
await usecases.deleteCampaignParticipationForAdmin({
userId,
campaignParticipationId,
});
return h.response({}).code(204);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import bluebird from 'bluebird';

const deleteCampaignParticipationForAdmin = async function ({
import { withTransaction } from '../../../../shared/domain/DomainTransaction.js';

const deleteCampaignParticipationForAdmin = withTransaction(async function ({
userId,
campaignParticipationId,
domainTransaction,
campaignRepository,
campaignParticipationRepository,
}) {
Expand All @@ -13,14 +14,13 @@ const deleteCampaignParticipationForAdmin = async function ({
await campaignParticipationRepository.getAllCampaignParticipationsInCampaignForASameLearner({
campaignId,
campaignParticipationId,
domainTransaction,
});

await bluebird.mapSeries(campaignParticipations, async (campaignParticipation) => {
campaignParticipation.delete(userId);
const { id, deletedAt, deletedBy } = campaignParticipation;
await campaignParticipationRepository.remove({ id, deletedAt, deletedBy, domainTransaction });
await campaignParticipationRepository.remove({ id, deletedAt, deletedBy });
});
};
});

export { deleteCampaignParticipationForAdmin };
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NotFoundError } from '../../../../../lib/domain/errors.js';
import { Campaign } from '../../../../../lib/domain/models/Campaign.js';
import * as knowledgeElementRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-repository.js';
import * as knowledgeElementSnapshotRepository from '../../../../../lib/infrastructure/repositories/knowledge-element-snapshot-repository.js';
import { DomainTransaction } from '../../../../shared/domain/DomainTransaction.js';
import { Assessment } from '../../../../shared/domain/models/Assessment.js';
import { ApplicationTransaction } from '../../../shared/infrastructure/ApplicationTransaction.js';
import { CampaignParticipation } from '../../domain/models/CampaignParticipation.js';
Expand Down Expand Up @@ -54,12 +55,8 @@ const get = async function (id, domainTransaction) {
});
};

const getAllCampaignParticipationsInCampaignForASameLearner = async function ({
campaignId,
campaignParticipationId,
domainTransaction,
}) {
const knexConn = domainTransaction.knexTransaction;
const getAllCampaignParticipationsInCampaignForASameLearner = async function ({ campaignId, campaignParticipationId }) {
const knexConn = DomainTransaction.getConnection();
const result = await knexConn('campaign-participations')
.select('organizationLearnerId')
.where({ id: campaignParticipationId, campaignId })
Expand Down Expand Up @@ -97,8 +94,8 @@ const getCampaignParticipationsForOrganizationLearner = async function ({ organi
);
};

const remove = async function ({ id, deletedAt, deletedBy, domainTransaction }) {
const knexConn = domainTransaction.knexTransaction;
const remove = async function ({ id, deletedAt, deletedBy }) {
const knexConn = DomainTransaction.getConnection();
return await knexConn('campaign-participations').where({ id }).update({ deletedAt, deletedBy });
};

Expand Down
34 changes: 30 additions & 4 deletions api/src/shared/domain/DomainTransaction.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import { AsyncLocalStorage } from 'node:async_hooks';

import { knex } from '../../../db/knex-database-connection.js';

const asyncLocalStorage = new AsyncLocalStorage();

class DomainTransaction {
constructor(knexTransaction) {
this.knexTransaction = knexTransaction;
}

static execute(lambda) {
static execute(lambda, transactionConfig) {
return knex.transaction((trx) => {
return lambda(new DomainTransaction(trx));
});
const domainTransaction = new DomainTransaction(trx);
return asyncLocalStorage.run({ transaction: domainTransaction }, lambda, domainTransaction);
}, transactionConfig);
}

static getConnection() {
const store = asyncLocalStorage.getStore();

if (store?.transaction) {
const domainTransaction = store.transaction;
return domainTransaction.knexTransaction;
}
return knex;
}

static emptyTransaction() {
return new DomainTransaction(null);
}
}
export { DomainTransaction };

/**
* @template F
* @param {F} func
* @param {import('knex').Knex.TransactionConfig | undefined} transactionConfig
* @returns {F}
*/
function withTransaction(func, transactionConfig) {
return (...args) => DomainTransaction.execute(() => func(...args), transactionConfig);
}

export { asyncLocalStorage, DomainTransaction, withTransaction };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { knex } from '../../../../../../db/knex-database-connection.js';
import { DomainTransaction } from '../../../../../../lib/infrastructure/DomainTransaction.js';
import { SessionAlreadyPublishedError } from '../../../../../../src/certification/session-management/domain/errors.js';
import { unfinalizeSession } from '../../../../../../src/certification/session-management/domain/usecases/unfinalize-session.js';
import { catchErr, expect, sinon } from '../../../../../test-helper.js';
Expand All @@ -10,7 +10,7 @@ describe('Unit | UseCase | unfinalize-session', function () {
describe('when session is not published', function () {
it('should call repositories with transaction', async function () {
// given
sinon.stub(knex, 'transaction').callsFake((fn) => fn());
sinon.stub(DomainTransaction, 'execute').callsFake((fn) => fn({}));

sessionRepository = {
unfinalize: sinon.stub(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CampaignParticipation } from '../../../../../../src/prescription/campaign-participation/domain/models/CampaignParticipation.js';
import { usecases } from '../../../../../../src/prescription/campaign-participation/domain/usecases/index.js';
import { DomainTransaction } from '../../../../../../src/shared/domain/DomainTransaction.js';
import { domainBuilder, expect, sinon } from '../../../../../test-helper.js';

const { deleteCampaignParticipationForAdmin } = usecases;
Expand All @@ -11,6 +12,10 @@ describe('Unit | UseCase | delete-campaign-participation-for-admin', function ()

beforeEach(function () {
clock = sinon.useFakeTimers({ now: now.getTime(), toFake: ['Date'] });
sinon.stub(DomainTransaction, 'execute');
DomainTransaction.execute.callsFake((fn) => {
return fn({});
});
});

afterEach(function () {
Expand All @@ -26,7 +31,6 @@ describe('Unit | UseCase | delete-campaign-participation-for-admin', function ()
remove: sinon.stub(),
};
const campaignParticipationId = 1234;
const domainTransaction = Symbol('domainTransaction');
const campaignId = domainBuilder.buildCampaign().id;
const ownerId = domainBuilder.buildUser().id;
const organizationLearnerId = domainBuilder.buildOrganizationLearner().id;
Expand All @@ -52,15 +56,13 @@ describe('Unit | UseCase | delete-campaign-participation-for-admin', function ()
.withArgs({
campaignId,
campaignParticipationId,
domainTransaction,
})
.resolves(campaignParticipations);

//when
await deleteCampaignParticipationForAdmin({
userId: ownerId,
campaignParticipationId,
domainTransaction,
campaignRepository,
campaignParticipationRepository,
});
Expand All @@ -77,7 +79,6 @@ describe('Unit | UseCase | delete-campaign-participation-for-admin', function ()
id: deletedCampaignParticipation.id,
deletedAt: deletedCampaignParticipation.deletedAt,
deletedBy: deletedCampaignParticipation.deletedBy,
domainTransaction,
});
});
});
Expand Down
100 changes: 100 additions & 0 deletions api/tests/shared/unit/domain/DomainTransaction_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
asyncLocalStorage,
DomainTransaction,
withTransaction,
} from '../../../../src/shared/domain/DomainTransaction.js';
import { expect, knex, sinon } from '../../../../tests/test-helper.js';

describe('Unit | Infrastructure | DomainTransaction', function () {
describe('#getConnection', function () {
it('should return connection from store', function () {
const transaction = Symbol('transaction');
const domainTransaction = new DomainTransaction(transaction);
const storeStub = { transaction: domainTransaction };
sinon.stub(asyncLocalStorage, 'getStore');
asyncLocalStorage.getStore.returns(storeStub);

const connection = DomainTransaction.getConnection();

expect(connection).to.equal(transaction);
});

it('should return knex connection by default', function () {
sinon.stub(asyncLocalStorage, 'getStore');

const connection = DomainTransaction.getConnection();

expect(connection).to.equal(knex);
});
});

describe('#execute', function () {
it('should store transaction', async function () {
const transactionStub = {};
const domainTransaction = new DomainTransaction(transactionStub);
sinon.stub(asyncLocalStorage, 'run');
sinon.stub(knex, 'transaction');
knex.transaction.callsFake((fn) => fn(transactionStub));

await DomainTransaction.execute(function () {
// Something
});

expect(asyncLocalStorage.run).to.have.been.calledWith({ transaction: domainTransaction });
});

it('should return function result', async function () {
const transactionConfiguration = { isolationLevel: 'read committed' };
const expectedResult = Symbol('return');
sinon.stub(knex, 'transaction');
knex.transaction.callsFake((fn) => fn({}));

await DomainTransaction.execute(() => expectedResult, transactionConfiguration);

expect(knex.transaction.getCalls()[0].args).to.includes(transactionConfiguration);
});

it('should use configuration for transaction', async function () {
const transactionStub = {};
const domainTransaction = new DomainTransaction(transactionStub);
sinon.stub(asyncLocalStorage, 'run');
sinon.stub(knex, 'transaction');
knex.transaction.callsFake((fn) => fn(transactionStub));

await DomainTransaction.execute(function () {
// Something
});

expect(asyncLocalStorage.run).to.have.been.calledWith({ transaction: domainTransaction });
});
});

describe('#withTransaction', function () {
it('should get transaction from store', async function () {
const transactionStub = { commit: sinon.stub() };
sinon.stub(knex, 'transaction');
knex.transaction.callsFake(() => transactionStub);
const myUseCase = withTransaction(() => {
return DomainTransaction.getConnection();
});
const connection = await myUseCase();

expect(connection).to.equal(transactionStub);
});

it('should use configuration for transaction', async function () {
const transactionStub = {};
const domainTransaction = new DomainTransaction(transactionStub);
sinon.stub(asyncLocalStorage, 'run');
sinon.stub(knex, 'transaction');
knex.transaction.callsFake((fn) => fn(transactionStub));

const myUseCase = withTransaction(function () {
// Something
});
await myUseCase();

expect(asyncLocalStorage.run).to.have.been.calledWith({ transaction: domainTransaction });
});
});
});

0 comments on commit 295f2b2

Please sign in to comment.