-
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 a script to send invitations
- Loading branch information
1 parent
7838a8e
commit e7188d3
Showing
4 changed files
with
201 additions
and
0 deletions.
There are no files selected for viewing
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
88 changes: 88 additions & 0 deletions
88
api/src/team/scripts/send-organization-invitations-script.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,88 @@ | ||
import { setTimeout } from 'node:timers/promises'; | ||
|
||
import Joi from 'joi'; | ||
import _ from 'lodash'; | ||
|
||
import { csvFileParser } from '../../shared/application/scripts/parsers.js'; | ||
import { Script } from '../../shared/application/scripts/script.js'; | ||
import { ScriptRunner } from '../../shared/application/scripts/script-runner.js'; | ||
import { OrganizationInvitation } from '../domain/models/OrganizationInvitation.js'; | ||
import { usecases } from '../domain/usecases/index.js'; | ||
|
||
const DEFAULT_BATCH_SIZE = 200; | ||
const DEFAULT_THROTTE_DELAY = 1000; | ||
|
||
const columnsSchemas = [ | ||
{ name: 'Organization ID', schema: Joi.number() }, | ||
{ name: 'email', schema: Joi.string().trim().replace(' ', '').lowercase() }, | ||
{ name: 'locale', schema: Joi.string().trim().lowercase() }, | ||
{ | ||
name: 'role', | ||
schema: Joi.string() | ||
.trim() | ||
.optional() | ||
.valid(...Object.values(OrganizationInvitation.RoleType)), | ||
}, | ||
]; | ||
|
||
export class SendOrganizationInvitationsScript extends Script { | ||
constructor() { | ||
super({ | ||
description: 'this script used to send invitations for one organization with a csv file', | ||
permanent: true, | ||
options: { | ||
dryRun: { | ||
type: 'boolean', | ||
describe: 'Just to check, not action', | ||
default: false, | ||
}, | ||
file: { | ||
type: 'string', | ||
describe: 'The file path', | ||
demandOption: true, | ||
coerce: csvFileParser(columnsSchemas), | ||
}, | ||
batchSize: { | ||
type: 'number', | ||
describe: 'The batch size', | ||
default: DEFAULT_BATCH_SIZE, | ||
}, | ||
throttleDelay: { | ||
type: 'number', | ||
describe: 'The throttle delay', | ||
default: DEFAULT_THROTTE_DELAY, | ||
}, | ||
}, | ||
}); | ||
} | ||
|
||
async handle({ options, logger, sendOrganizationInvitation = usecases.createOrganizationInvitationByAdmin }) { | ||
const { file, dryRun, batchSize, throttleDelay } = options; | ||
const batches = _.chunk(file, batchSize); | ||
let batchCount = 1; | ||
for (const batch of batches) { | ||
logger.info(`Batch #${batchCount++}`); | ||
batch.map(async (invitation) => { | ||
if (!dryRun) { | ||
await sendOrganizationInvitation({ | ||
organizationId: invitation['Organization ID'], | ||
email: invitation.email, | ||
locale: invitation.locale, | ||
role: invitation.role || null, | ||
}); | ||
} | ||
}); | ||
await setTimeout(throttleDelay); | ||
if (dryRun) { | ||
logger.info('Dry run, no action'); | ||
} | ||
} | ||
if (dryRun) { | ||
logger.info(`${file.length} invitations will be processed`); | ||
} else { | ||
logger.info(`${file.length} invitations processed`); | ||
} | ||
} | ||
} | ||
|
||
await ScriptRunner.execute(import.meta.url, SendOrganizationInvitationsScript); |
2 changes: 2 additions & 0 deletions
2
api/tests/team/unit/scripts/files/send-organization-invitations.csv
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,2 @@ | ||
"Organization ID","email","locale","role" | ||
1,[email protected],fr,MEMBER |
110 changes: 110 additions & 0 deletions
110
api/tests/team/unit/scripts/send-organization-invitations-script_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,110 @@ | ||
import * as url from 'node:url'; | ||
|
||
import { SendOrganizationInvitationsScript } from '../../../../src/team/scripts/send-organization-invitations-script.js'; | ||
import { expect, sinon } from '../../../test-helper.js'; | ||
|
||
const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); | ||
|
||
describe('unit | team | scripts | send-organization-invitations-script', function () { | ||
describe('options', function () { | ||
it('Check the csv parser', async function () { | ||
// given | ||
const testCsvFile = `${currentDirectory}files/send-organization-invitations.csv`; | ||
const script = new SendOrganizationInvitationsScript(); | ||
|
||
// when | ||
const { options } = script.metaInfo; | ||
const fileData = await options.file.coerce(testCsvFile); | ||
|
||
// then | ||
expect(fileData).to.be.deep.equal([ | ||
{ 'Organization ID': 1, email: '[email protected]', locale: 'fr', role: 'MEMBER' }, | ||
]); | ||
}); | ||
}); | ||
|
||
describe('#handle', function () { | ||
let file; | ||
let script; | ||
let logger; | ||
let sendOrganizationInvitation; | ||
|
||
beforeEach(function () { | ||
file = [ | ||
{ 'Organization Id': 1, email: '[email protected]', locale: 'fr', role: 'MEMBER' }, | ||
{ 'Organization Id': 2, email: '[email protected]', locale: 'en', role: 'ADMIN' }, | ||
{ 'Organization Id': 3, email: '[email protected]', locale: 'fr', role: 'MEMBER' }, | ||
]; | ||
script = new SendOrganizationInvitationsScript(); | ||
logger = { info: sinon.spy() }; | ||
sendOrganizationInvitation = sinon.stub(); | ||
}); | ||
|
||
it('Runs the script', async function () { | ||
// when | ||
await script.handle({ | ||
options: { file }, | ||
logger, | ||
sendOrganizationInvitation, | ||
}); | ||
|
||
// then | ||
expect(sendOrganizationInvitation).to.have.callCount(3); | ||
expect(sendOrganizationInvitation).to.have.calledWith({ | ||
organizationId: file[0]['Organization ID'], | ||
email: file[0].email, | ||
locale: file[0].locale, | ||
role: file[0].role, | ||
}); | ||
expect(logger.info).to.have.been.calledWith('3 invitations processed'); | ||
}); | ||
|
||
it('runs the script and replace empty role by null', async function () { | ||
// given | ||
const file = [{ 'Organization ID': 1, email: '[email protected]', locale: 'fr' }]; | ||
|
||
// when | ||
await script.handle({ | ||
options: { file }, | ||
logger, | ||
sendOrganizationInvitation, | ||
}); | ||
|
||
// then | ||
expect(sendOrganizationInvitation).to.have.been.calledWith({ | ||
organizationId: 1, | ||
email: '[email protected]', | ||
locale: 'fr', | ||
role: null, | ||
}); | ||
}); | ||
|
||
it('Runs the script with batch', async function () { | ||
// when | ||
await script.handle({ | ||
options: { file, batchSize: 1 }, | ||
logger, | ||
sendOrganizationInvitation, | ||
}); | ||
|
||
// then | ||
expect(logger.info).to.have.been.calledWith('Batch #1'); | ||
expect(logger.info).to.have.been.calledWith('Batch #2'); | ||
expect(logger.info).to.have.been.calledWith('Batch #3'); | ||
}); | ||
|
||
it('runs the script with dryRun', async function () { | ||
// when | ||
await script.handle({ | ||
options: { file, dryRun: true }, | ||
logger, | ||
sendOrganizationInvitation, | ||
}); | ||
|
||
// then | ||
expect(logger.info).to.have.been.calledWith('Dry run, no action'); | ||
expect(sendOrganizationInvitation).to.not.have.been.called; | ||
expect(logger.info).to.have.been.calledWith('3 invitations will be processed'); | ||
}); | ||
}); | ||
}); |