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

Commit f3a0e27

Browse files
committed
feat(cli): create command to generate CSV with course participants (#399)
1 parent 9e1015a commit f3a0e27

14 files changed

+196
-48
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ out
5555
# Generated
5656
packages/ui/src/svg
5757
packages/ui/src/icons
58+
packages/cli/output
5859
.cache-loader
5960

6061
# Database

packages/cli/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
"csv-parse": "4.16.3",
1414
"dotenv": "10.0.0",
1515
"generate-password": "1.6.1",
16+
"json2csv": "5.0.6",
1617
"puppeteer": "10.4.0",
1718
"reflect-metadata": "0.1.13",
1819
"winston": "3.3.3"
20+
},
21+
"devDependencies": {
22+
"@types/json2csv": "5.0.3"
1923
}
2024
}

packages/cli/src/commands/generate-checklists.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,14 @@ export const generateChecklists = (program: Command) => {
5353
logger.debug('Iterating through fetched participants');
5454

5555
for (const participant of participants) {
56-
const checklist = await generateProcessStChecklist(participant.name);
57-
58-
await updateUserById(participant.id, { checklist });
56+
if (!participant.checklist) {
57+
const name = `${participant.firstName} ${participant.lastName}`;
58+
const checklist = await generateProcessStChecklist(name);
59+
60+
await updateUserById(participant.id, { checklist });
61+
} else {
62+
logger.debug('Participant already has a checklist', participant);
63+
}
5964
}
6065

6166
logger.debug('Iteration through fetched participants finished');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Command } from 'commander';
2+
3+
import { parseToCsv } from '../shared/csv';
4+
import { getUsersByRole } from '../shared/db';
5+
import { createWelcomeCsv } from '../shared/files';
6+
import { createLogger } from '../shared/logger';
7+
import { userRoles, WelcomeCsvRow } from '../shared/models';
8+
import { transformToMatchClass } from '../shared/object';
9+
10+
const logger = createLogger('generate-welcome-csv');
11+
12+
export const generateWelcomeCsv = (program: Command) => {
13+
program
14+
.command('generate-welcome-csv')
15+
.description('Generates CSV file with names, emails and checklist URLs for all participants in the database')
16+
.action(async () => {
17+
try {
18+
const participants = await getUsersByRole(userRoles.participant);
19+
const welcomeCsvRows = await Promise.all(participants.map(transformToMatchClass(WelcomeCsvRow)));
20+
21+
logger.debug('Parsing welcome CSV objects to CSV format', welcomeCsvRows);
22+
23+
const csv = parseToCsv(welcomeCsvRows);
24+
25+
logger.debug('Creating welcome CSVfile', csv);
26+
27+
await createWelcomeCsv(csv);
28+
} catch (ex) {
29+
logger.error(ex);
30+
}
31+
});
32+
};
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Argument, Command } from 'commander';
2+
import { generate as generatePassword } from 'generate-password';
23

34
import { getCsvContent } from '../shared/csv';
4-
import { insertUsers, register } from '../shared/db';
5+
import { getUsersByRole, insertUsers, register } from '../shared/db';
56
import { validateEnv } from '../shared/env';
7+
import { createPasswordsList } from '../shared/files';
68
import { createLogger } from '../shared/logger';
79
import { ParticipantCsvRow, RegisterDTO, User, userRoles } from '../shared/models';
810
import { transformToMatchClass } from '../shared/object';
@@ -15,34 +17,47 @@ export const registerParticipants = (program: Command) => {
1517
.description('Creates accounts for CodersCamp participants listed in the CSV file')
1618
.addArgument(new Argument('<csv-path>', 'Path to the CSV file'))
1719
.action(async (csvPath: string) => {
20+
const participants: User[] = [];
21+
const passwords = createPasswordsList();
22+
1823
try {
1924
await validateEnv();
2025

2126
const rows = await getCsvContent(csvPath);
2227
const participantsRows = await Promise.all(rows.map(transformToMatchClass(ParticipantCsvRow)));
23-
24-
const participants: User[] = [];
28+
const currentParticipants = await getUsersByRole(userRoles.participant);
29+
const currentParticipantsEmails = currentParticipants.map(({ email }) => email);
2530

2631
logger.debug('Iterating through parsed rows');
2732

2833
for (const { email, firstName, lastName } of participantsRows) {
29-
const registerDto = await transformToMatchClass(RegisterDTO)({ email });
30-
const userId = await register(registerDto);
31-
const participant = await transformToMatchClass(User)({
32-
...registerDto,
33-
id: userId,
34-
name: `${firstName} ${lastName}`,
35-
role: userRoles.participant,
36-
});
37-
38-
participants.push(participant);
34+
if (!currentParticipantsEmails.includes(email)) {
35+
const password = generatePassword({ length: 16, numbers: true, symbols: true });
36+
const registerDto = await transformToMatchClass(RegisterDTO)({ email, password });
37+
const userId = await register(registerDto);
38+
const participant = await transformToMatchClass(User)({
39+
...registerDto,
40+
id: userId,
41+
firstName,
42+
lastName,
43+
role: userRoles.participant,
44+
});
45+
46+
participants.push(participant);
47+
passwords.add({ email, password });
48+
} else {
49+
logger.debug(`Participant with email ${email} already exists in the database`);
50+
}
3951
}
4052

4153
logger.debug('Iteration through parsed rows finished');
4254

4355
await insertUsers(participants);
4456
} catch (ex) {
4557
logger.error(ex);
58+
logger.info('Already registered participants', participants);
59+
} finally {
60+
await passwords.save();
4661
}
4762
});
4863
};

packages/cli/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'reflect-metadata';
33
import { Command } from 'commander';
44

55
import { generateChecklists } from './commands/generate-checklists';
6+
import { generateWelcomeCsv } from './commands/generate-welcome-csv';
67
import { registerParticipants } from './commands/register-participants';
78

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

1213
registerParticipants(program);
1314
generateChecklists(program);
15+
generateWelcomeCsv(program);
1416

1517
program.parse();

packages/cli/src/shared/csv.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import parse from 'csv-parse/lib/sync';
1+
import parseFromCsv from 'csv-parse/lib/sync';
22
import { readFile } from 'fs/promises';
33

44
import { createLogger } from './logger';
55

6+
export { parse as parseToCsv } from 'json2csv';
7+
68
const logger = createLogger('CSV Utils');
79

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

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

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

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

packages/cli/src/shared/db.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { createClient } from '@supabase/supabase-js';
2-
import { generate } from 'generate-password';
32

43
import { env } from './env';
54
import { createLogger } from './logger';
@@ -14,10 +13,7 @@ const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);
1413
export const register = async (registerDto: RegisterDTO): Promise<User['id']> => {
1514
logger.debug(`Registering user with email ${registerDto.email}`);
1615

17-
const { user, error } = await supabase.auth.signUp({
18-
email: registerDto.email,
19-
password: generate({ length: 16, numbers: true, symbols: true }),
20-
});
16+
const { user, error } = await supabase.auth.signUp(registerDto);
2117

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

packages/cli/src/shared/env.ts

+1-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Expose, plainToClass } from 'class-transformer';
2-
import { IsInt, IsNotEmpty, IsString, validateOrReject } from 'class-validator';
2+
import { IsNotEmpty, IsString, validateOrReject } from 'class-validator';
33
import dotenv from 'dotenv';
44

55
import { createLogger } from './logger';
@@ -23,26 +23,6 @@ class EnvVariables {
2323
@IsString()
2424
@IsNotEmpty()
2525
PROCESS_ST_CHECKLIST_URL: string;
26-
27-
@Expose()
28-
@IsInt()
29-
// @IsNotEmpty()
30-
NODEMAILER_PORT: number;
31-
32-
@Expose()
33-
@IsString()
34-
// @IsNotEmpty()
35-
NODEMAILER_HOST: string;
36-
37-
@Expose()
38-
@IsString()
39-
// @IsNotEmpty()
40-
NODEMAILER_USER: string;
41-
42-
@Expose()
43-
@IsString()
44-
// @IsNotEmpty()
45-
NODEMAILER_PASSWORD: string;
4626
}
4727

4828
export const env = plainToClass(EnvVariables, process.env, {

packages/cli/src/shared/files.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { existsSync } from 'fs';
2+
import { mkdir, writeFile } from 'fs/promises';
3+
4+
import { createLogger } from './logger';
5+
6+
const logger = createLogger('Files Utils');
7+
8+
const createOutputDir = async () => {
9+
if (!existsSync('output')) {
10+
await mkdir('output');
11+
}
12+
};
13+
14+
export const createPasswordsList = () => {
15+
const passwords: Record<string, string> = {};
16+
17+
const add = ({ email, password }: { email: string; password: string }) => {
18+
logger.debug(`Adding password for email ${email}`);
19+
passwords[email] = password;
20+
};
21+
22+
const save = async () => {
23+
await createOutputDir();
24+
25+
const path = 'output/passwords.json';
26+
27+
logger.debug(`Saving passwords object to ${path}`);
28+
await writeFile(path, JSON.stringify(passwords), 'utf-8');
29+
logger.debug('Passwords saved successfully');
30+
};
31+
32+
return { add, save };
33+
};
34+
35+
export const createWelcomeCsv = async (csv: string) => {
36+
await createOutputDir();
37+
38+
const path = 'output/welcome.csv';
39+
40+
logger.debug(`Saving welcome CSV to ${path}`);
41+
await writeFile(path, csv, 'utf-8');
42+
logger.debug('Welcome CSV saved successfully');
43+
};

packages/cli/src/shared/models.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export class RegisterDTO {
3333
@IsEmail()
3434
@IsNotEmpty()
3535
email: string;
36+
37+
@Expose()
38+
@IsString()
39+
@IsNotEmpty()
40+
password: string;
3641
}
3742

3843
export class User {
@@ -49,7 +54,12 @@ export class User {
4954
@Expose()
5055
@IsString()
5156
@IsNotEmpty()
52-
name: string;
57+
firstName: string;
58+
59+
@Expose()
60+
@IsString()
61+
@IsNotEmpty()
62+
lastName: string;
5363

5464
@Expose()
5565
@IsUrl()
@@ -60,3 +70,20 @@ export class User {
6070
@IsIn(Object.values(userRoles))
6171
role: Role;
6272
}
73+
74+
export class WelcomeCsvRow {
75+
@Expose()
76+
@IsString()
77+
@IsNotEmpty()
78+
firstName: string;
79+
80+
@Expose()
81+
@IsEmail()
82+
@IsNotEmpty()
83+
email: string;
84+
85+
@Expose()
86+
@IsUrl()
87+
@IsNotEmpty()
88+
checklist: string;
89+
}

packages/cli/src/shared/object.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { ClassConstructor, plainToClass } from 'class-transformer';
22
import { validateOrReject } from 'class-validator';
33

4-
import type { AnyObject, UnknownObject } from '@coderscamp/shared/types';
4+
import type { AnyObject } from '@coderscamp/shared/types';
55

66
import { createLogger } from './logger';
77

88
const logger = createLogger('Object utils');
99

1010
export const transformToMatchClass =
1111
<ClassInstance extends AnyObject>(cls: ClassConstructor<ClassInstance>) =>
12-
async <Obj extends UnknownObject | ClassInstance>(obj: Obj): Promise<ClassInstance> => {
12+
async <Obj extends AnyObject | ClassInstance>(obj: Obj): Promise<ClassInstance> => {
1313
logger.debug(`Transforming object to match the ${cls.name} class`, obj);
1414

1515
const result = plainToClass(cls, obj, { excludeExtraneousValues: true, enableImplicitConversion: true });

packages/cli/test.csv

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
firstName,lastName,email
2+
3+
Edna,Edwards,[email protected]

0 commit comments

Comments
 (0)