Skip to content

Commit

Permalink
feat(api): add a script to populate campaignParticipationId in keSnap…
Browse files Browse the repository at this point in the history
…shot
  • Loading branch information
lionelB committed Jan 15, 2025
1 parent 6e2c472 commit bacf2fc
Show file tree
Hide file tree
Showing 2 changed files with 308 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { knex } from '../../db/knex-database-connection.js';
import { CampaignTypes } from '../../src/prescription/shared/domain/constants.js';
import { Script } from '../../src/shared/application/scripts/script.js';
import { ScriptRunner } from '../../src/shared/application/scripts/script-runner.js';

const DEFAULT_CHUNK_SIZE = 10000;
const DEFAULT_PAUSE_DURATION = 2000;

const pause = async (duration) => {
return Promise((resolve) => {
setTimeout(resolve, duration);
});
};

// Définition du script
export class PopulateCampaignParticipationIdScript extends Script {
constructor() {
super({
description:
'This script will populate the column knowledge-element-snapshots.campaignParticipationId with campaign-participations.id',
permanent: true,
options: {
chunkSize: {
type: 'number',
default: DEFAULT_CHUNK_SIZE,
description: 'number of records to update in one update',
},
pauseDuration: {
type: 'number',
default: DEFAULT_PAUSE_DURATION,
description: 'Time in ms between each chunk processing',
},
},
});
}

async handle({ options, logger, dependencies = { pause } }) {
const result = await knex('knowledge-element-snapshots').count().whereNull('campaignParticipationId').first();
logger.info(`Try to populate ${result.count} missing campaignParticipationId`);

let [firstId] = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.orderBy('id', 'asc')
.limit(1)
.pluck('id');

let updatedRows;

do {
// We remove one because sql between is inclusive [ -- ] and we want that [ -- [
const lastId = firstId + options.chunkSize - 1;
updatedRows = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.updateFrom('campaign-participations')
.update({
campaignParticipationId: knex.ref('campaign-participations.id'),
})
.where('knowledge-element-snapshots.snappedAt', knex.ref('campaign-participations.sharedAt'))
.where('knowledge-element-snapshots.userId', knex.ref('campaign-participations.userId'))
.whereBetween('knowledge-element-snapshots.id', [firstId, lastId]);
logger.info(`update ${updatedRows} rows from "knowledge-element-snapshots" (firstId:${firstId})`);
firstId += options.chunkSize;
if (updatedRows > 0 && options.pauseDuration > 0) {
await dependencies.pause(options.pauseDuration);
}
} while (updatedRows > 0);

let emptyRowResult = await knex('knowledge-element-snapshots').count().whereNull('campaignParticipationId').first();
logger.info(`${emptyRowResult.count} rows with empty campaignParticipationId to update`);

const anonymisedParticipations = await knex('campaign-participations')
.select(['campaign-participations.id', 'sharedAt'])
.join('campaigns', function () {
this.on('campaigns.id', 'campaign-participations.campaignId').onVal('campaigns.type', CampaignTypes.ASSESSMENT);
})
.whereNull('userId')
.whereNotNull('sharedAt');

const emptyKeSnapshotParticipations = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.select(['id', 'snappedAt']);

logger.info(
`Try to populate ${emptyKeSnapshotParticipations.lenght} keSnapshot from ${anonymisedParticipations} anonimised participations`,
);

const matchingSnapshotAndparticipations = emptyKeSnapshotParticipations.flatMap(({ id, snappedAt }) => {
const participations = anonymisedParticipations.filter(({ sharedAt }) => {
console.log(
snappedAt.toISOString(),
sharedAt.toISOString(),
snappedAt.toISOString() === sharedAt.toISOString(),
);
return snappedAt.toISOString() === sharedAt.toISOString();
});
if (participations.length != 1) {
return [];
}
return [{ keSnapshotId: id, campaignParticipationId: participations[0].id }];
});

const trx = await knex.transaction();
for (const row of matchingSnapshotAndparticipations) {
await trx('knowledge-element-snapshots')
.update({ campaignParticipationId: row.campaignParticipationId })
.where({ id: row.keSnapshotId });
}
await trx.commit();

logger.info(`Populate ${matchingSnapshotAndparticipations.lenght} anonymised participations`);

emptyRowResult = await knex('knowledge-element-snapshots').count().whereNull('campaignParticipationId').first();
logger.info(`${emptyRowResult.count} rows with empty campaignParticipationId to update`);
}
}

// Exécution du script
await ScriptRunner.execute(import.meta.url, PopulateCampaignParticipationIdScript);
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { PopulateCampaignParticipationIdScript } from '../../../../scripts/prod/populate-campaign-participation-id-in-knowledge-element-snapshot.js';
import { databaseBuilder, expect, knex, sinon } from '../../../test-helper.js';

describe('Script | Prod | Delete Organization Learners From Organization', function () {
describe('Options', function () {
it('has the correct options', function () {
const script = new PopulateCampaignParticipationIdScript();
const { options } = script.metaInfo;
expect(options.chunkSize).to.deep.include({
type: 'number',
default: 10000,
description: 'number of records to update in one update',
});
expect(options.pauseDuration).to.deep.include({
type: 'number',
default: 2000,
description: 'Time in ms between each chunk processing',
});
});
});

describe('Handle', function () {
let script;
let logger;
let dependencies;
let user, learner, campaign, otherCampaign, snapshot1, snapshot2;

beforeEach(async function () {
script = new PopulateCampaignParticipationIdScript();
logger = { info: sinon.spy(), error: sinon.spy(), debug: sinon.spy() };
dependencies = { pause: sinon.stub() };

user = databaseBuilder.factory.buildUser({ id: 123, firstName: 'Sam', lastName: 'Sagace' });
learner = databaseBuilder.factory.buildOrganizationLearner({
userId: user.id,
});

campaign = databaseBuilder.factory.buildCampaign();
otherCampaign = databaseBuilder.factory.buildCampaign();
await databaseBuilder.commit();
});

describe('linked participation', function () {
beforeEach(async function () {
const participation = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId: learner.id,
userId: user.id,
campaign: campaign.id,
participantExternalId: null,
createdAt: new Date('2024-12-15'),
sharedAt: new Date('2024-12-16'),
});
snapshot1 = databaseBuilder.factory.knowledgeElementSnapshotFactory.buildSnapshot({
id: 1,
userId: user.id,
snappedAt: participation.sharedAt,
knowledgeElementsAttributes: [{ skillId: 'skill_1', status: 'validated', earnedPix: 40 }],
});
const otherParticipation = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId: learner.id,
userId: user.id,
campaign: otherCampaign.id,
participantExternalId: null,
createdAt: new Date('2024-05-09'),
sharedAt: new Date('2024-05-12'),
});
snapshot2 = databaseBuilder.factory.knowledgeElementSnapshotFactory.buildSnapshot({
id: 2,
userId: user.id,
snappedAt: otherParticipation.sharedAt,
knowledgeElementsAttributes: [{ skillId: 'skill_1', status: 'validated', earnedPix: 40 }],
});
await databaseBuilder.commit();
});

it('log how many entries will be updated', async function () {
await script.handle({ options: { chunkSize: 1, pauseDuration: 0 }, logger, dependencies });
expect(logger.info.calledWithExactly('Try to populate 2 missing campaignParticipationId')).to.be.true;
});

it('populate empty participations one by one', async function () {
await script.handle({ options: { chunkSize: 1, pauseDuration: 0 }, logger, dependencies });
const emptyKeSnaptshots = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.count()
.first();
expect(emptyKeSnaptshots.count).to.equals(0);
expect(logger.info).to.have.been.calledWith(
`update 1 rows from "knowledge-element-snapshots" (firstId:${snapshot1.id})`,
);
expect(logger.info).to.have.been.calledWith(
`update 1 rows from "knowledge-element-snapshots" (firstId:${snapshot2.id})`,
);
expect(logger.info).to.have.been.calledWith('0 rows with empty campaignParticipationId to update');
});

it('populate empty participations using a chunk of 1000', async function () {
await script.handle({ options: { chunkSize: 1000, pauseDuration: 0 }, logger, dependencies });
const emptyKeSnaptshots = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.count()
.first();
expect(emptyKeSnaptshots.count).to.equals(0);
expect(logger.info).to.have.been.calledWith(
`update 2 rows from "knowledge-element-snapshots" (firstId:${snapshot1.id})`,
);
expect(logger.info).to.have.been.calledWith('0 rows with empty campaignParticipationId to update');
});

it('should pause between chunk of 1', async function () {
dependencies.pause.resolves();
await script.handle({ options: { chunkSize: 1, pauseDuration: 10 }, logger, dependencies });
const emptyKeSnaptshots = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.count()
.first();

expect(emptyKeSnaptshots.count).to.equals(0);
expect(dependencies.pause).to.have.been.calledTwice;
expect(dependencies.pause).to.have.been.calledWith(10);
});
});

describe('anonymised participations', function () {
it('should populate matching unique participation and snapshot', async function () {
//when
const participation = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId: learner.id,
userId: null,
campaign: campaign.id,
participantExternalId: null,
createdAt: new Date('2024-12-15'),
sharedAt: new Date('2014-12-16'),
});

databaseBuilder.factory.knowledgeElementSnapshotFactory.buildSnapshot({
userId: user.id,
snappedAt: participation.sharedAt,
knowledgeElementsAttributes: [{ skillId: 'skill_1', status: 'validated', earnedPix: 40 }],
});
await databaseBuilder.commit();

//given
await script.handle({ options: { chunkSize: 1000, pauseDuration: 0 }, logger, dependencies });

//then
const matchingSnapshot = await knex('knowledge-element-snapshots')
.where('snappedAt', participation.sharedAt)
.first();
expect(matchingSnapshot.campaignParticipationId).to.equals(participation.id);
});

it('should not populate when matching multiple participations and snapshot', async function () {
// given
const participation = databaseBuilder.factory.buildCampaignParticipation({
organizationLearnerId: learner.id,
userId: null,
campaign: campaign.id,
participantExternalId: null,
createdAt: new Date('2024-12-15'),
sharedAt: new Date('2014-12-16'),
});
databaseBuilder.factory.buildCampaignParticipation({
userId: null,
participantExternalId: null,
createdAt: new Date('2024-12-15'),
sharedAt: participation.sharedAt,
});

databaseBuilder.factory.knowledgeElementSnapshotFactory.buildSnapshot({
userId: user.id,
snappedAt: participation.sharedAt,
knowledgeElementsAttributes: [{ skillId: 'skill_1', status: 'validated', earnedPix: 40 }],
});
await databaseBuilder.commit();

// then
await script.handle({ options: { chunkSize: 1000, pauseDuration: 0 }, logger, dependencies });

//then
const emptyKeSnaptshots = await knex('knowledge-element-snapshots')
.whereNull('campaignParticipationId')
.count()
.first();

expect(emptyKeSnaptshots.count).to.equals(1);
});
});
});
});

0 comments on commit bacf2fc

Please sign in to comment.