From 6f85786efbea5be7493da0ed36c35b430c287233 Mon Sep 17 00:00:00 2001 From: mtp0326 Date: Wed, 6 Dec 2023 22:54:27 -0500 Subject: [PATCH 1/5] batch service added --- server/src/services/batch.service.ts | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 server/src/services/batch.service.ts diff --git a/server/src/services/batch.service.ts b/server/src/services/batch.service.ts new file mode 100644 index 00000000..3626a7a2 --- /dev/null +++ b/server/src/services/batch.service.ts @@ -0,0 +1,153 @@ +/** + * Service that contains functions to process asynchronous concurrent operations. + * Input is a list or lists of items, number of asynchronous operations to run concurrently, and a function to process each item. + * Output is a list of results from the function using Promise. + */ + +/** + * A function that takes in a list of items and + * a function to process each item in a async batch with Promise. + * @param fn The function of the request. + * @param limit The limit of promise request that can be done at once. + * @param items List of items. + * @returns The response of batch requests in a list{@link results} + */ +const batchReturnList = async ( + fn: (item: any) => Promise, + limit: number, + items: Array, +) => { + try { + let results: any[] = []; + for (let start = 0; start < items.length; start += limit) { + const end = start + limit > items.length ? items.length : start + limit; + + const slicedResults = await Promise.all(items.slice(start, end).map(fn)); + + results = [...results, ...slicedResults]; + } + + return results; + } catch (error) { + console.error('An error occurred in one of the promises:', error); + + throw error; + } +}; + +/** + * A function that takes in a list of items and + * a function to process each item in a async batch with Promise. + * @param fn The function of the request. + * @param limit The limit of promise request that can be done at once. + * @param items List of items. + * @returns void + */ +const batchReturnVoid = async ( + fn: (item: any) => void, + limit: number, + items: Array, +) => { + try { + for (let start = 0; start < items.length; start += limit) { + const end = start + limit > items.length ? items.length : start + limit; + + await Promise.all(items.slice(start, end).map(fn)); + } + + return; + } catch (error) { + console.error('An error occurred in one of the promises:', error); + + throw error; + } +}; + +/** + * A function that takes in a variable amount of lists in parameters and + * a function to process each item in a async batch with Promise. + * @param fn The function of the request. + * @param limit The limit of promise request that can be done at once. + * @param lists List of params. + * @returns The response of batch requests in a list{@link results} + */ +const batchReturnMultiList = async >( + fn: (...args: any[]) => any, + limit: number, + ...lists: T[] +) => { + try { + let results: any[] = []; + + const minLength = Math.min(...lists.map((list) => list.length)); + + for (let start = 0; start < minLength; start += limit) { + const end = start + limit > minLength ? minLength : start + limit; + const slicedLists = await Promise.all( + lists.map((list) => list.slice(start, end)), + ); + + const promises = Array.from({ length: end - start }, async (_, i) => { + const elements = slicedLists.map((arr) => arr[i]); + return fn(...elements); + }); + + const slicedResults = await Promise.all(promises); + + results = [...results, ...slicedResults]; + } + + return results; + } catch (error) { + console.error('An error occurred in one of the promises:', error); + + throw error; + } +}; + +/** + * A function that takes in a variable amount of lists in parameters and + * a function to process each item in a async batch with Promise. + * @param lists List of params. + * @param limit The limit of promise request that can be done at once. + * @param fn The function of the request. + * @returns void + */ +const batchReturnMultiListVoid = async >( + fn: (...args: any[]) => void, + limit: number, + ...lists: T[] +) => { + try { + const minLength = Math.min(...lists.map((list) => list.length)); + + for (let start = 0; start < minLength; start += limit) { + const end = start + limit > minLength ? minLength : start + limit; + const slicedLists = await Promise.all( + lists.map((list) => list.slice(start, end)), + ); + + const promises = Array.from({ length: end - start }, async (_, i) => { + // Collect elements from each array at index `i` + const elements = slicedLists.map((arr) => arr[i]); + // Apply the function `fn` to these elements + return fn(...elements); + }); + + await Promise.all(promises); + } + + return; + } catch (error) { + console.error('An error occurred in one of the promises:', error); + + throw error; + } +}; + +export { + batchReturnList, + batchReturnVoid, + batchReturnMultiList, + batchReturnMultiListVoid, +}; From 4226071a0db7ffc1d6b53861959a6283cefe0bdb Mon Sep 17 00:00:00 2001 From: mtp0326 Date: Sat, 9 Dec 2023 11:57:47 -0500 Subject: [PATCH 2/5] batch fixed --- .../components/buttons/InviteUserButton.tsx | 4 +- server/src/controllers/admin.controller.ts | 102 ++++++++++++---- server/src/services/batch.service.ts | 111 +----------------- 3 files changed, 88 insertions(+), 129 deletions(-) diff --git a/client/src/components/buttons/InviteUserButton.tsx b/client/src/components/buttons/InviteUserButton.tsx index bac20cb4..d2400073 100644 --- a/client/src/components/buttons/InviteUserButton.tsx +++ b/client/src/components/buttons/InviteUserButton.tsx @@ -51,7 +51,9 @@ function InviteUserButton() { - Please enter the email address of the user you would like to invite. + Please enter one or more email addresses separated by commas and + role of the user you would like to invite. (ex. a@gmail.com, + b@outlook.com) { - const { email } = req.body; + const { email, role } = req.body; + if (!email) { + next(ApiError.missingFields(['email'])); + return; + } + let emailList = email.replaceAll(' ', '').split(','); const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; - if (!email.match(emailRegex)) { - next(ApiError.badRequest('Invalid email')); - } - const lowercaseEmail = email.toLowerCase(); - const existingUser: IUser | null = await getUserByEmail(lowercaseEmail); - if (existingUser) { - next( - ApiError.badRequest( - `An account with email ${lowercaseEmail} already exists.`, - ), - ); + + function validateEmail(email: string) { + if (!email.match(emailRegex)) { + throw new Error('Invalid email'); + } return; } - const existingInvite: IInvite | null = await getInviteByEmail(lowercaseEmail); + function validateNewUser(user: IUser) { + if (user) { + throw new Error(`An account with email ${user.email} already exists.`); + } + return; + } - try { + function combineEmailToken(email: string, invite: IInvite | null) { const verificationToken = crypto.randomBytes(32).toString('hex'); - if (existingInvite) { - await updateInvite(existingInvite, verificationToken); - } else { - await createInvite(lowercaseEmail, verificationToken); + return [email, invite, verificationToken]; + } + + async function makeInvite(combinedList: any[]) { + try { + const email = combinedList[0]; + const existingInvite = combinedList[1]; + const verificationToken = combinedList[2]; + if (existingInvite) { + await updateInvite(existingInvite, verificationToken); + } else { + await createInvite(email, verificationToken); + } + } catch (err: any) { + throw new Error('Error creating invite'); + } + } + + function sendInvite(combinedList: any[]) { + try { + const email = combinedList[0]; + const verificationToken = combinedList[2]; + + emailInviteLink(email, verificationToken); + return; + } catch (err: any) { + throw new Error('Error sending invite'); } + } + + try { + await batchMultiInput(validateEmail, 10, emailList); + const lowercaseEmailList: string[] | null = await batchMultiInput( + async (email: string) => { + return email.toLowerCase(); + }, + 10, + emailList, + ); + + const existingUserList: any[] | null = await batchMultiInput( + getUserByEmail, + 10, + lowercaseEmailList, + ); + await batchMultiInput(validateNewUser, 10, existingUserList); + + const existingInviteList: any[] | null = await batchMultiInput( + getInviteByEmail, + 10, + lowercaseEmailList, + ); + const emailInviteList: any[] = await batchMultiInput( + combineEmailToken, + 10, + lowercaseEmailList, + existingInviteList, + ); + + await batchMultiInput(makeInvite, 10, emailInviteList); + await batchMultiInput(sendInvite, 10, emailInviteList); - await emailInviteLink(lowercaseEmail, verificationToken); res.sendStatus(StatusCode.CREATED); - } catch (err) { - next(ApiError.internal('Unable to invite user.')); + } catch (err: any) { + next(ApiError.internal('Unable to invite user: ' + err.message)); } }; diff --git a/server/src/services/batch.service.ts b/server/src/services/batch.service.ts index 3626a7a2..11cde25a 100644 --- a/server/src/services/batch.service.ts +++ b/server/src/services/batch.service.ts @@ -1,68 +1,10 @@ /** * Service that contains functions to process asynchronous concurrent operations. - * Input is a list or lists of items, number of asynchronous operations to run concurrently, and a function to process each item. + * Input is a function to process each item, number of asynchronous operations to run + * concurrently, and a list or lists of items. * Output is a list of results from the function using Promise. */ -/** - * A function that takes in a list of items and - * a function to process each item in a async batch with Promise. - * @param fn The function of the request. - * @param limit The limit of promise request that can be done at once. - * @param items List of items. - * @returns The response of batch requests in a list{@link results} - */ -const batchReturnList = async ( - fn: (item: any) => Promise, - limit: number, - items: Array, -) => { - try { - let results: any[] = []; - for (let start = 0; start < items.length; start += limit) { - const end = start + limit > items.length ? items.length : start + limit; - - const slicedResults = await Promise.all(items.slice(start, end).map(fn)); - - results = [...results, ...slicedResults]; - } - - return results; - } catch (error) { - console.error('An error occurred in one of the promises:', error); - - throw error; - } -}; - -/** - * A function that takes in a list of items and - * a function to process each item in a async batch with Promise. - * @param fn The function of the request. - * @param limit The limit of promise request that can be done at once. - * @param items List of items. - * @returns void - */ -const batchReturnVoid = async ( - fn: (item: any) => void, - limit: number, - items: Array, -) => { - try { - for (let start = 0; start < items.length; start += limit) { - const end = start + limit > items.length ? items.length : start + limit; - - await Promise.all(items.slice(start, end).map(fn)); - } - - return; - } catch (error) { - console.error('An error occurred in one of the promises:', error); - - throw error; - } -}; - /** * A function that takes in a variable amount of lists in parameters and * a function to process each item in a async batch with Promise. @@ -71,7 +13,7 @@ const batchReturnVoid = async ( * @param lists List of params. * @returns The response of batch requests in a list{@link results} */ -const batchReturnMultiList = async >( +const batchMultiInput = async >( fn: (...args: any[]) => any, limit: number, ...lists: T[] @@ -105,49 +47,4 @@ const batchReturnMultiList = async >( } }; -/** - * A function that takes in a variable amount of lists in parameters and - * a function to process each item in a async batch with Promise. - * @param lists List of params. - * @param limit The limit of promise request that can be done at once. - * @param fn The function of the request. - * @returns void - */ -const batchReturnMultiListVoid = async >( - fn: (...args: any[]) => void, - limit: number, - ...lists: T[] -) => { - try { - const minLength = Math.min(...lists.map((list) => list.length)); - - for (let start = 0; start < minLength; start += limit) { - const end = start + limit > minLength ? minLength : start + limit; - const slicedLists = await Promise.all( - lists.map((list) => list.slice(start, end)), - ); - - const promises = Array.from({ length: end - start }, async (_, i) => { - // Collect elements from each array at index `i` - const elements = slicedLists.map((arr) => arr[i]); - // Apply the function `fn` to these elements - return fn(...elements); - }); - - await Promise.all(promises); - } - - return; - } catch (error) { - console.error('An error occurred in one of the promises:', error); - - throw error; - } -}; - -export { - batchReturnList, - batchReturnVoid, - batchReturnMultiList, - batchReturnMultiListVoid, -}; +export { batchMultiInput }; From 04b4d5f627ed35f3f955037661b1fe697ac019cb Mon Sep 17 00:00:00 2001 From: Rose Wang <51464298+rosewang01@users.noreply.github.com> Date: Thu, 16 May 2024 23:44:52 -0400 Subject: [PATCH 3/5] bug fixes to batch service --- server/src/controllers/admin.controller.ts | 75 +++++++++++----------- server/src/services/batch.service.ts | 50 --------------- 2 files changed, 38 insertions(+), 87 deletions(-) delete mode 100644 server/src/services/batch.service.ts diff --git a/server/src/controllers/admin.controller.ts b/server/src/controllers/admin.controller.ts index ece83031..4bef245a 100644 --- a/server/src/controllers/admin.controller.ts +++ b/server/src/controllers/admin.controller.ts @@ -21,7 +21,6 @@ import { } from '../services/invite.service'; import { IInvite } from '../models/invite.model'; import { emailInviteLink } from '../services/mail.service'; -import { batchMultiInput } from '../services/batch.service'; /** * Get all users from the database. Upon success, send the a list of all users in the res body with 200 OK status code. @@ -141,12 +140,12 @@ const inviteUser = async ( res: express.Response, next: express.NextFunction, ) => { - const { email, role } = req.body; - if (!email) { + const { emails } = req.body; + if (!emails) { next(ApiError.missingFields(['email'])); return; } - let emailList = email.replaceAll(' ', '').split(','); + const emailList = emails.replaceAll(' ', '').split(','); const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; @@ -154,14 +153,6 @@ const inviteUser = async ( if (!email.match(emailRegex)) { throw new Error('Invalid email'); } - return; - } - - function validateNewUser(user: IUser) { - if (user) { - throw new Error(`An account with email ${user.email} already exists.`); - } - return; } function combineEmailToken(email: string, invite: IInvite | null) { @@ -197,40 +188,50 @@ const inviteUser = async ( } try { - await batchMultiInput(validateEmail, 10, emailList); - const lowercaseEmailList: string[] | null = await batchMultiInput( - async (email: string) => { - return email.toLowerCase(); - }, - 10, - emailList, + if (emailList.length === 0) { + next(ApiError.missingFields(['email'])); + return; + } + emailList.forEach(validateEmail); + const lowercaseEmailList: string[] = emailList.map((email: string) => + email.toLowerCase(), ); - const existingUserList: any[] | null = await batchMultiInput( - getUserByEmail, - 10, - lowercaseEmailList, - ); - await batchMultiInput(validateNewUser, 10, existingUserList); + const userPromises = lowercaseEmailList.map(getUserByEmail); + const existingUserList = await Promise.all(userPromises); + + const invitePromises = lowercaseEmailList.map(getInviteByEmail); + const existingInviteList = await Promise.all(invitePromises); - const existingInviteList: any[] | null = await batchMultiInput( - getInviteByEmail, - 10, - lowercaseEmailList, + const existingUserEmails = existingUserList.map((user) => + user ? user.email : '', ); - const emailInviteList: any[] = await batchMultiInput( - combineEmailToken, - 10, - lowercaseEmailList, - existingInviteList, + const existingInviteEmails = existingInviteList.map((invite) => + invite ? invite.email : '', ); - await batchMultiInput(makeInvite, 10, emailInviteList); - await batchMultiInput(sendInvite, 10, emailInviteList); + const emailInviteList = lowercaseEmailList.filter((email) => { + if (existingUserEmails.includes(email)) { + throw new Error(`An account with email ${email} already exists.`); + } + return !existingUserEmails.includes(email); + }); + + const combinedList = emailInviteList.map((email) => { + const existingInvite = + existingInviteList[existingInviteEmails.indexOf(email)]; + return combineEmailToken(email, existingInvite); + }); + + const makeInvitePromises = combinedList.map(makeInvite); + await Promise.all(makeInvitePromises); + + const sendInvitePromises = combinedList.map(sendInvite); + await Promise.all(sendInvitePromises); res.sendStatus(StatusCode.CREATED); } catch (err: any) { - next(ApiError.internal('Unable to invite user: ' + err.message)); + next(ApiError.internal(`Unable to invite user: ${err.message}`)); } }; diff --git a/server/src/services/batch.service.ts b/server/src/services/batch.service.ts deleted file mode 100644 index 11cde25a..00000000 --- a/server/src/services/batch.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Service that contains functions to process asynchronous concurrent operations. - * Input is a function to process each item, number of asynchronous operations to run - * concurrently, and a list or lists of items. - * Output is a list of results from the function using Promise. - */ - -/** - * A function that takes in a variable amount of lists in parameters and - * a function to process each item in a async batch with Promise. - * @param fn The function of the request. - * @param limit The limit of promise request that can be done at once. - * @param lists List of params. - * @returns The response of batch requests in a list{@link results} - */ -const batchMultiInput = async >( - fn: (...args: any[]) => any, - limit: number, - ...lists: T[] -) => { - try { - let results: any[] = []; - - const minLength = Math.min(...lists.map((list) => list.length)); - - for (let start = 0; start < minLength; start += limit) { - const end = start + limit > minLength ? minLength : start + limit; - const slicedLists = await Promise.all( - lists.map((list) => list.slice(start, end)), - ); - - const promises = Array.from({ length: end - start }, async (_, i) => { - const elements = slicedLists.map((arr) => arr[i]); - return fn(...elements); - }); - - const slicedResults = await Promise.all(promises); - - results = [...results, ...slicedResults]; - } - - return results; - } catch (error) { - console.error('An error occurred in one of the promises:', error); - - throw error; - } -}; - -export { batchMultiInput }; From 323c18d9217258588f8ee4586a08b7611f5ebdad Mon Sep 17 00:00:00 2001 From: Rose Wang <51464298+rosewang01@users.noreply.github.com> Date: Thu, 16 May 2024 23:51:56 -0400 Subject: [PATCH 4/5] fixed error handling --- server/src/controllers/admin.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/controllers/admin.controller.ts b/server/src/controllers/admin.controller.ts index 4bef245a..c3a9f951 100644 --- a/server/src/controllers/admin.controller.ts +++ b/server/src/controllers/admin.controller.ts @@ -151,7 +151,7 @@ const inviteUser = async ( function validateEmail(email: string) { if (!email.match(emailRegex)) { - throw new Error('Invalid email'); + next(ApiError.badRequest(`Invalid email: ${email}`)); } } @@ -171,7 +171,7 @@ const inviteUser = async ( await createInvite(email, verificationToken); } } catch (err: any) { - throw new Error('Error creating invite'); + next(ApiError.internal(`Error creating invite: ${err.message}`)); } } @@ -183,7 +183,7 @@ const inviteUser = async ( emailInviteLink(email, verificationToken); return; } catch (err: any) { - throw new Error('Error sending invite'); + next(ApiError.internal(`Error sending invite: ${err.message}`)); } } @@ -212,7 +212,7 @@ const inviteUser = async ( const emailInviteList = lowercaseEmailList.filter((email) => { if (existingUserEmails.includes(email)) { - throw new Error(`An account with email ${email} already exists.`); + next(ApiError.badRequest(`User with email ${email} already exists`)); } return !existingUserEmails.includes(email); }); From 2b5e5c459b28bf6897fd799575806131a79755da Mon Sep 17 00:00:00 2001 From: Rose Wang <51464298+rosewang01@users.noreply.github.com> Date: Fri, 17 May 2024 00:25:53 -0400 Subject: [PATCH 5/5] bug fixes --- .../src/components/buttons/InviteUserButton.tsx | 15 +++++++-------- server/src/controllers/admin.controller.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/client/src/components/buttons/InviteUserButton.tsx b/client/src/components/buttons/InviteUserButton.tsx index d2400073..41a0684b 100644 --- a/client/src/components/buttons/InviteUserButton.tsx +++ b/client/src/components/buttons/InviteUserButton.tsx @@ -11,7 +11,7 @@ import { postData } from '../../util/api'; function InviteUserButton() { const [open, setOpen] = useState(false); - const [email, setEmail] = useState(''); + const [emails, setEmails] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); @@ -27,11 +27,11 @@ function InviteUserButton() { const handleInvite = async () => { setLoading(true); - postData('admin/invite', { email }).then((res) => { + postData('admin/invite', { emails }).then((res) => { if (res.error) { setError(res.error.message); } else { - setAlert(`${email} successfully invited!`, AlertType.SUCCESS); + setAlert(`${emails} successfully invited!`, AlertType.SUCCESS); setOpen(false); } setLoading(false); @@ -40,7 +40,7 @@ function InviteUserButton() { const updateEmail = (event: React.ChangeEvent) => { setError(''); - setEmail(event.target.value); + setEmails(event.target.value); }; return ( @@ -51,15 +51,14 @@ function InviteUserButton() { - Please enter one or more email addresses separated by commas and - role of the user you would like to invite. (ex. a@gmail.com, - b@outlook.com) + Please enter one or more email addresses separated by commas. (ex. + a@gmail.com, b@outlook.com) { if (existingUserEmails.includes(email)) { - next(ApiError.badRequest(`User with email ${email} already exists`)); + throw ApiError.badRequest(`User with email ${email} already exists`); } return !existingUserEmails.includes(email); });