-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): add organization share script
- Loading branch information
1 parent
5a787a7
commit f708049
Showing
3 changed files
with
216 additions
and
0 deletions.
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
api/db/database-builder/factory/build-organization-profile-reward.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import _ from 'lodash'; | ||
|
||
import { ORGANIZATIONS_PROFILE_REWARDS_TABLE_NAME } from '../../migrations/20241118134739_create-organizations-profile-rewards-table.js'; | ||
import { databaseBuffer } from '../database-buffer.js'; | ||
import { buildOrganization } from './build-organization.js'; | ||
import { buildProfileReward } from './build-profile-reward.js'; | ||
|
||
export const buildOrganizationProfileReward = ({ | ||
id = databaseBuffer.getNextId(), | ||
organizationId, | ||
profileRewardId, | ||
} = {}) => { | ||
organizationId = _.isUndefined(organizationId) ? buildOrganization().id : organizationId; | ||
profileRewardId = _.isUndefined(profileRewardId) ? buildProfileReward().id : profileRewardId; | ||
|
||
const values = { | ||
id, | ||
organizationId, | ||
profileRewardId, | ||
}; | ||
|
||
return databaseBuffer.pushInsertable({ | ||
tableName: ORGANIZATIONS_PROFILE_REWARDS_TABLE_NAME, | ||
values, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { ORGANIZATIONS_PROFILE_REWARDS_TABLE_NAME } from '../../../db/migrations/20241118134739_create-organizations-profile-rewards-table.js'; | ||
import { PGSQL_UNIQUE_CONSTRAINT_VIOLATION_ERROR } from '../../../db/pgsql-errors.js'; | ||
import { Script } from '../../shared/application/scripts/script.js'; | ||
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js'; | ||
import { DomainTransaction } from '../../shared/domain/DomainTransaction.js'; | ||
|
||
const options = { | ||
limit: { | ||
type: 'number', | ||
describe: 'Id limit', | ||
demandOption: true, | ||
requiresArg: true, | ||
coerce: Number, | ||
}, | ||
offset: { | ||
type: 'number', | ||
describe: 'Id offset', | ||
demandOption: true, | ||
requiresArg: true, | ||
coerce: Number, | ||
}, | ||
}; | ||
|
||
export class SixthGradeOrganizationShare extends Script { | ||
constructor() { | ||
super({ | ||
description: 'Insert share attestations with organizations for sixth graders', | ||
permanent: false, | ||
options, | ||
}); | ||
} | ||
|
||
async handle({ options, logger }) { | ||
const profileRewards = await this.fetchProfileRewards(options.limit, options.offset); | ||
|
||
logger.info(`${profileRewards.length} users to handle`); | ||
|
||
let count = 1; | ||
|
||
for (const profileReward of profileRewards) { | ||
logger.info(`Handling user ${profileReward.userId}: (${count}/${profileRewards.length})`); | ||
|
||
const userOrganizationIds = await this.fetchUserOrganizations(profileReward.userId); | ||
|
||
logger.info(`Organization ids for user ${profileReward.userId}: ${userOrganizationIds.join(',')}`); | ||
|
||
for (const organizationId of userOrganizationIds) { | ||
const knexConnection = await DomainTransaction.getConnection(); | ||
logger.info(`Table insertion for user ${profileReward.userId} and organization ${organizationId}`); | ||
|
||
try { | ||
await knexConnection(ORGANIZATIONS_PROFILE_REWARDS_TABLE_NAME).insert({ | ||
profileRewardId: profileReward.id, | ||
organizationId, | ||
}); | ||
} catch (error) { | ||
if (error.code === PGSQL_UNIQUE_CONSTRAINT_VIOLATION_ERROR) { | ||
logger.warn( | ||
`User ${profileReward.userId} already shared an attestation with organization ${organizationId}`, | ||
); | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
count++; | ||
} | ||
} | ||
|
||
/** | ||
* @param {number} limit | ||
* @param {number} offset | ||
* | ||
* @returns {Promise<[{id:number, userId:number}]>} | ||
*/ | ||
async fetchProfileRewards(limit, offset) { | ||
const knexConnection = DomainTransaction.getConnection(); | ||
return await knexConnection('profile-rewards').select('userId', 'id').limit(limit).offset(offset); | ||
} | ||
|
||
async fetchUserOrganizations(userId) { | ||
const knexConnection = DomainTransaction.getConnection(); | ||
const organizations = await knexConnection('view-active-organization-learners') | ||
.select('organizationId') | ||
.where({ userId }); | ||
|
||
return organizations.map(({ organizationId }) => organizationId); | ||
} | ||
} | ||
|
||
await ScriptRunner.execute(import.meta.url, SixthGradeOrganizationShare); |
98 changes: 98 additions & 0 deletions
98
api/tests/profile/integration/scripts/sixth-grade-organization-share_test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import sinon from 'sinon'; | ||
|
||
import { SixthGradeOrganizationShare } from '../../../../src/profile/scripts/sixth-grade-organization-share.js'; | ||
import { databaseBuilder, expect, knex } from '../../../test-helper.js'; | ||
|
||
describe('Integration | Profile | Scripts | sixth-grade-organization-share ', function () { | ||
describe('#handle', function () { | ||
let organizationProfileRewards; | ||
let logger; | ||
let profileRewardIds; | ||
let firstOrganizationId; | ||
let secondOrganizationId; | ||
|
||
before(async function () { | ||
// build attestation | ||
const attestation = databaseBuilder.factory.buildAttestation(); | ||
|
||
// build users | ||
const userIds = [...Array(8).keys()].map((id) => databaseBuilder.factory.buildUser({ id: id + 1 }).id); | ||
|
||
// build organizations | ||
firstOrganizationId = databaseBuilder.factory.buildOrganization().id; | ||
secondOrganizationId = databaseBuilder.factory.buildOrganization().id; | ||
|
||
// build organization learners | ||
userIds.forEach((userId) => | ||
databaseBuilder.factory.buildOrganizationLearner({ organizationId: firstOrganizationId, userId }), | ||
); | ||
|
||
// build an other organization learner for userId 3 | ||
databaseBuilder.factory.buildOrganizationLearner({ organizationId: secondOrganizationId, userId: 3 }); | ||
|
||
// build profile rewards | ||
profileRewardIds = userIds.map( | ||
(userId) => databaseBuilder.factory.buildProfileReward({ rewardId: attestation.id, userId }).id, | ||
); | ||
|
||
// build one organization profile reward to test unique constraint violation | ||
databaseBuilder.factory.buildOrganizationProfileReward({ | ||
profileRewardId: profileRewardIds[4], | ||
organizationId: firstOrganizationId, | ||
}); | ||
|
||
await databaseBuilder.commit(); | ||
|
||
const script = new SixthGradeOrganizationShare(); | ||
logger = { info: sinon.spy(), warn: sinon.spy() }; | ||
|
||
await script.handle({ | ||
options: { | ||
limit: 5, | ||
offset: 2, | ||
}, | ||
logger, | ||
}); | ||
|
||
organizationProfileRewards = await knex('organizations-profile-rewards').select('*'); | ||
}); | ||
|
||
it('should handle offset option', async function () { | ||
const organizationProfilRewardIds = organizationProfileRewards.map( | ||
(profileReward) => profileReward.profileRewardId, | ||
); | ||
expect(organizationProfilRewardIds).to.not.contains(profileRewardIds[0]); | ||
expect(organizationProfilRewardIds).to.not.contains(profileRewardIds[1]); | ||
}); | ||
|
||
it('should handle limit option', async function () { | ||
const organizationProfilRewardIds = organizationProfileRewards.map( | ||
(profileReward) => profileReward.profileRewardId, | ||
); | ||
expect(organizationProfilRewardIds).to.not.contains(profileRewardIds[7]); | ||
expect(organizationProfilRewardIds).to.not.contains(profileRewardIds[8]); | ||
}); | ||
|
||
it('should handle pgsql unique constraint violation error', async function () { | ||
expect(logger.warn).to.have.been.calledOnceWithExactly( | ||
`User 5 already shared an attestation with organization ${firstOrganizationId}`, | ||
); | ||
}); | ||
|
||
it('should insert expected data', async function () { | ||
const organizationProfileRewardsWithoutIds = organizationProfileRewards.map((organizationProfileReward) => { | ||
delete organizationProfileReward.id; | ||
return organizationProfileReward; | ||
}); | ||
|
||
expect(organizationProfileRewardsWithoutIds).to.have.deep.members([ | ||
{ organizationId: firstOrganizationId, profileRewardId: profileRewardIds[2] }, | ||
{ organizationId: secondOrganizationId, profileRewardId: profileRewardIds[2] }, | ||
{ organizationId: firstOrganizationId, profileRewardId: profileRewardIds[3] }, | ||
{ organizationId: firstOrganizationId, profileRewardId: profileRewardIds[4] }, | ||
{ organizationId: firstOrganizationId, profileRewardId: profileRewardIds[5] }, | ||
{ organizationId: firstOrganizationId, profileRewardId: profileRewardIds[6] }, | ||
]); | ||
}); | ||
}); | ||
}); |