Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

Commit

Permalink
feat(cli): create command to generate CSV with course participants (#399
Browse files Browse the repository at this point in the history
)
  • Loading branch information
KonradSzwarc committed Oct 28, 2021
1 parent 9e1015a commit b888236
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 87 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ out
# Generated
packages/ui/src/svg
packages/ui/src/icons
packages/cli/output
.cache-loader

# Database
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
"commander": "8.2.0",
"csv-parse": "4.16.3",
"dotenv": "10.0.0",
"generate-password": "1.6.1",
"json2csv": "5.0.6",
"puppeteer": "10.4.0",
"reflect-metadata": "0.1.13",
"winston": "3.3.3"
},
"devDependencies": {
"@types/json2csv": "5.0.3"
}
}
11 changes: 8 additions & 3 deletions packages/cli/src/commands/generate-checklists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,14 @@ export const generateChecklists = (program: Command) => {
logger.debug('Iterating through fetched participants');

for (const participant of participants) {
const checklist = await generateProcessStChecklist(participant.name);

await updateUserById(participant.id, { checklist });
if (!participant.checklist) {
const name = `${participant.firstName} ${participant.lastName}`;
const checklist = await generateProcessStChecklist(name);

await updateUserById(participant.id, { checklist });
} else {
logger.debug('Participant already has a checklist', participant);
}
}

logger.debug('Iteration through fetched participants finished');
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/commands/generate-welcome-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isInt, isPositive } from 'class-validator';
import { Argument, Command } from 'commander';

import { parseToCsv } from '../shared/csv';
import { getUsersByRole } from '../shared/db';
import { createWelcomeCsv } from '../shared/files';
import { createLogger } from '../shared/logger';
import { userRoles, WelcomeCsvRow } from '../shared/models';
import { transformAndValidate } from '../shared/object';

const logger = createLogger('generate-welcome-csv');

export const generateWelcomeCsv = (program: Command) => {
program
.command('generate-welcome-csv')
.description('Generates CSV file with names, emails and checklist URLs for all participants in the database')
.addArgument(new Argument('<from-id>', 'Path to the CSV file'))
.action(async (fromId?: string) => {
const startFromId = Number(fromId);

try {
if (fromId && !(isPositive(startFromId) && isInt(startFromId))) {
throw new Error('fromId parameter must be a positive integer');
}

let participants = await getUsersByRole(userRoles.participant);

if (startFromId) {
participants = participants.filter((p) => p.id >= startFromId);
}

const welcomeCsvRows = await Promise.all(participants.map(transformAndValidate(WelcomeCsvRow)));

logger.debug('Parsing welcome CSV objects to CSV format', welcomeCsvRows);

const csv = parseToCsv(welcomeCsvRows);

logger.debug('Creating welcome CSVfile', csv);

await createWelcomeCsv(csv);
} catch (ex) {
logger.error(ex);
}
});
};
47 changes: 29 additions & 18 deletions packages/cli/src/commands/register-participants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Argument, Command } from 'commander';

import { asyncFilter, removeDuplicatesForProperty } from '../shared/array';
import { getCsvContent } from '../shared/csv';
import { insertUsers, register } from '../shared/db';
import { getUsersByRole, insertUsers } from '../shared/db';
import { validateEnv } from '../shared/env';
import { createLogger } from '../shared/logger';
import { ParticipantCsvRow, RegisterDTO, User, userRoles } from '../shared/models';
import { transformToMatchClass } from '../shared/object';
import { CreateUserDTO, ParticipantCsvRow, userRoles } from '../shared/models';
import { filterInvalid, transformAndValidate, transformToMatchClass } from '../shared/object';

const logger = createLogger('register-participants');

Expand All @@ -20,27 +21,37 @@ export const registerParticipants = (program: Command) => {

const rows = await getCsvContent(csvPath);
const participantsRows = await Promise.all(rows.map(transformToMatchClass(ParticipantCsvRow)));
const correctParticipantsRows = await asyncFilter(participantsRows, filterInvalid(ParticipantCsvRow));

const participants: User[] = [];
const currentParticipants = await getUsersByRole(userRoles.participant);
const currentParticipantsEmails = currentParticipants.map(({ email }) => email);

logger.debug('Iterating through parsed rows');
logger.debug('Filtering emails that are already added to the database');

for (const { email, firstName, lastName } of participantsRows) {
const registerDto = await transformToMatchClass(RegisterDTO)({ email });
const userId = await register(registerDto);
const participant = await transformToMatchClass(User)({
...registerDto,
id: userId,
name: `${firstName} ${lastName}`,
role: userRoles.participant,
});
const participantsRowsToAdd = correctParticipantsRows.filter(({ email }) => {
if (currentParticipantsEmails.includes(email)) {
logger.debug(`Participant with email ${email} already exists in the database`);

participants.push(participant);
}
return false;
}

logger.debug('Iteration through parsed rows finished');
return true;
});

await insertUsers(participants);
logger.debug('Mapping ParticipantCsvRows to CreateUserDTOs');

const createUserDTOs = await Promise.all(
participantsRowsToAdd.map(({ email, firstName, lastName }) =>
transformAndValidate(CreateUserDTO)({
email,
firstName,
lastName,
role: userRoles.participant,
}),
),
);

await insertUsers(removeDuplicatesForProperty(createUserDTOs, 'email'));
} catch (ex) {
logger.error(ex);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'reflect-metadata';
import { Command } from 'commander';

import { generateChecklists } from './commands/generate-checklists';
import { generateWelcomeCsv } from './commands/generate-welcome-csv';
import { registerParticipants } from './commands/register-participants';

const program = new Command();
Expand All @@ -11,5 +12,6 @@ program.version('0.0.0');

registerParticipants(program);
generateChecklists(program);
generateWelcomeCsv(program);

program.parse();
11 changes: 11 additions & 0 deletions packages/cli/src/shared/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const asyncFilter = async <Item>(
arr: Item[],
predicate: (item: Item, index: number, arr: Item[]) => Promise<boolean>,
) => {
const results = await Promise.all(arr.map(predicate));

return arr.filter((_value, i) => results[i]);
};

export const removeDuplicatesForProperty = <Item>(arr: Item[], property: keyof Item) =>
arr.filter((value, i, array) => array.findIndex((t) => t[property] === value[property]) === i);
6 changes: 4 additions & 2 deletions packages/cli/src/shared/csv.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import parse from 'csv-parse/lib/sync';
import parseFromCsv from 'csv-parse/lib/sync';
import { readFile } from 'fs/promises';

import { createLogger } from './logger';

export { parse as parseToCsv } from 'json2csv';

const logger = createLogger('CSV Utils');

export const getCsvContent = async (csvPath: string) => {
Expand All @@ -12,7 +14,7 @@ export const getCsvContent = async (csvPath: string) => {

logger.debug('parsing content of the CSV file');

const parsedContent: Record<string, unknown>[] = parse(content, { columns: true });
const parsedContent: Record<string, unknown>[] = parseFromCsv(content, { columns: true });

logger.debug('CSV file content parsed successfully');

Expand Down
36 changes: 13 additions & 23 deletions packages/cli/src/shared/db.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
import { createClient } from '@supabase/supabase-js';
import { generate } from 'generate-password';

import { env } from './env';
import { createLogger } from './logger';
import { RegisterDTO, Role, User } from './models';
import { CreateUserDTO, Role, User } from './models';

const logger = createLogger('DB Utils');

const USERS_TABLE_NAME = 'users';

const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);

export const register = async (registerDto: RegisterDTO): Promise<User['id']> => {
logger.debug(`Registering user with email ${registerDto.email}`);

const { user, error } = await supabase.auth.signUp({
email: registerDto.email,
password: generate({ length: 16, numbers: true, symbols: true }),
});

if (!user) {
throw error ?? new Error(`Unknown error ocurred when signing up user with email ${registerDto.email}`);
}

logger.debug(`User with email ${registerDto.email} registered`, { id: user.id });

return user.id;
};

export const getUsersByRole = async (role: Role) => {
logger.debug(`Fetching users with the ${role} role`);

const { data, error } = await supabase.from<User>(USERS_TABLE_NAME).select().eq('role', role);

if (!data) {
if (!data || error) {
throw new Error(error ? error.message : `Unknown error ocurred when getting users from the database`);
}

Expand All @@ -42,18 +24,26 @@ export const getUsersByRole = async (role: Role) => {
return data;
};

export const insertUsers = async (users: User[]) => {
export const insertUsers = async (users: CreateUserDTO[]) => {
logger.debug(`Inserting provided users to the ${USERS_TABLE_NAME} table`, users);

await supabase.from<User>(USERS_TABLE_NAME).insert(users);
const { error } = await supabase.from<User>(USERS_TABLE_NAME).insert(users);

if (error) {
throw new Error(error.message);
}

logger.debug(`Users inserted to the ${USERS_TABLE_NAME} table`);
};

export const updateUserById = async (id: User['id'], data: Partial<Omit<User, 'id' | 'password'>>) => {
logger.debug(`Updating user with id ${id} using the provided data`, data);

await supabase.from<User>(USERS_TABLE_NAME).update(data).match({ id });
const { error } = await supabase.from<User>(USERS_TABLE_NAME).update(data).match({ id });

if (error) {
throw new Error(error.message);
}

logger.debug(`User with id ${id} updated successfully`);
};
22 changes: 1 addition & 21 deletions packages/cli/src/shared/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Expose, plainToClass } from 'class-transformer';
import { IsInt, IsNotEmpty, IsString, validateOrReject } from 'class-validator';
import { IsNotEmpty, IsString, validateOrReject } from 'class-validator';
import dotenv from 'dotenv';

import { createLogger } from './logger';
Expand All @@ -23,26 +23,6 @@ class EnvVariables {
@IsString()
@IsNotEmpty()
PROCESS_ST_CHECKLIST_URL: string;

@Expose()
@IsInt()
// @IsNotEmpty()
NODEMAILER_PORT: number;

@Expose()
@IsString()
// @IsNotEmpty()
NODEMAILER_HOST: string;

@Expose()
@IsString()
// @IsNotEmpty()
NODEMAILER_USER: string;

@Expose()
@IsString()
// @IsNotEmpty()
NODEMAILER_PASSWORD: string;
}

export const env = plainToClass(EnvVariables, process.env, {
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/shared/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { existsSync } from 'fs';
import { mkdir, writeFile } from 'fs/promises';

import { createLogger } from './logger';

const logger = createLogger('Files Utils');

const createOutputDir = async () => {
if (!existsSync('output')) {
await mkdir('output');
}
};

export const createPasswordsList = () => {
const passwords: Record<string, string> = {};

const add = ({ email, password }: { email: string; password: string }) => {
logger.debug(`Adding password for email ${email}`);
passwords[email] = password;
};

const save = async () => {
await createOutputDir();

const path = 'output/passwords.json';

logger.debug(`Saving passwords object to ${path}`);
await writeFile(path, JSON.stringify(passwords), 'utf-8');
logger.debug('Passwords saved successfully');
};

return { add, save };
};

export const createWelcomeCsv = async (csv: string) => {
await createOutputDir();

const path = 'output/welcome.csv';

logger.debug(`Saving welcome CSV to ${path}`);
await writeFile(path, csv, 'utf-8');
logger.debug('Welcome CSV saved successfully');
};
Loading

0 comments on commit b888236

Please sign in to comment.