Skip to content

Commit

Permalink
feat(api): Add a script to send invitations
Browse files Browse the repository at this point in the history
  • Loading branch information
theotime2005 authored Feb 5, 2025
1 parent 7838a8e commit e7188d3
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
1 change: 1 addition & 0 deletions api/src/team/domain/models/OrganizationInvitation.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ class OrganizationInvitation {
}

OrganizationInvitation.StatusType = statuses;
OrganizationInvitation.RoleType = roles;

export { OrganizationInvitation, statuses };
88 changes: 88 additions & 0 deletions api/src/team/scripts/send-organization-invitations-script.js
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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"Organization ID","email","locale","role"
1,[email protected],fr,MEMBER
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');
});
});
});

0 comments on commit e7188d3

Please sign in to comment.