-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LIME-69: Add Profile Settings #151
Changes from 29 commits
c0974e5
e15a9ab
32e6191
284b119
7f86ff6
ca7ee04
3fcd363
87de412
a637d6b
ab344fd
a9daac0
1ce32dd
70d3e64
76f0d87
a31057a
d494db6
d614b9e
fa09227
271bb57
94eed62
f93c242
106bf64
957f051
b3b8845
0b30a31
19038b8
4ad70ea
b60ace7
e529c9a
54420f6
84b73e3
fd0d8b8
b686c35
63dfd78
3f2c8e9
5d72fc5
b5ea42d
c057363
82ae302
f01a6fa
6ff19d5
6fc4c82
00c3120
adaf24d
e074b30
6c9f823
4be7f12
8c2dd31
6c0e7ec
6abee47
cd3b767
f82a456
03c78ee
27fce08
0162c32
b13522b
8c97740
c5bb5ef
c42c044
e66c71e
09edfd6
b4c7c0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
export { UserAttributes } from './user-attributes.enum.js'; | ||
export { UserDetailsAttributes } from './user-details-attributes.enum.js'; | ||
export { Gender, UsersApiPath } from 'shared'; | ||
export { | ||
AuthApiPath, | ||
HttpCode, | ||
HttpError, | ||
UserValidationMessage, | ||
} from 'shared'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,17 @@ | ||
import { UserValidationMessage } from 'shared'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. import |
||
|
||
import { type UserService } from '~/bundles/users/user.service.js'; | ||
import { type UserAuthResponseDto } from '~/bundles/users/users.js'; | ||
import { | ||
type UserAuthResponseDto, | ||
type UserUpdateProfileRequestDto, | ||
} from '~/bundles/users/users.js'; | ||
import { | ||
type ApiHandlerOptions, | ||
type ApiHandlerResponse, | ||
BaseController, | ||
} from '~/common/controller/controller.js'; | ||
import { BaseController } from '~/common/controller/controller.js'; | ||
import { ApiPath } from '~/common/enums/enums.js'; | ||
import { HttpCode } from '~/common/http/http.js'; | ||
import { HttpCode, HttpError } from '~/common/http/http.js'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. like here |
||
import { type Logger } from '~/common/logger/logger.js'; | ||
|
||
import { UsersApiPath } from './enums/enums.js'; | ||
|
@@ -95,6 +100,20 @@ class UserController extends BaseController { | |
}>, | ||
), | ||
}); | ||
|
||
this.addRoute({ | ||
path: `${UsersApiPath.UPDATE_USER}/:userId`, | ||
valery-chumak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
method: 'PATCH', | ||
isProtected: true, | ||
handler: (options) => | ||
this.updateUser( | ||
options as ApiHandlerOptions<{ | ||
user: UserAuthResponseDto; | ||
body: UserUpdateProfileRequestDto; | ||
params: { userId: string }; | ||
}>, | ||
), | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -167,6 +186,53 @@ class UserController extends BaseController { | |
payload: user, | ||
}; | ||
} | ||
|
||
private async updateUser( | ||
options: ApiHandlerOptions<{ | ||
user: UserAuthResponseDto; | ||
body: UserUpdateProfileRequestDto; | ||
params: { userId: string }; | ||
}>, | ||
): Promise<ApiHandlerResponse> { | ||
const { user, body, params } = options; | ||
const userId = params.userId; | ||
|
||
try { | ||
if (Number(userId) !== Number(user.id)) { | ||
throw new Error('Token mismatch'); | ||
} | ||
const updatedUser = await this.userService.update( | ||
Number(userId), | ||
body, | ||
); | ||
if (updatedUser && body.dateOfBirth) { | ||
const [day, month, year] = body.dateOfBirth.split('/'); | ||
const parsedDate = new Date(`${year}-${month}-${day}`); | ||
|
||
if (Number.isNaN(parsedDate.getTime())) { | ||
throw new HttpError({ | ||
message: UserValidationMessage.BIRTHDATE_FORMAT, | ||
status: HttpCode.BAD_REQUEST, | ||
}); | ||
} else { | ||
const formattedDate = | ||
parsedDate.toLocaleDateString('en-GB'); | ||
|
||
updatedUser.dateOfBirth = formattedDate; | ||
} | ||
} | ||
valery-chumak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return { | ||
status: HttpCode.OK, | ||
payload: updatedUser, | ||
}; | ||
} catch (error) { | ||
throw new HttpError({ | ||
message: `Something went wrong ${error}`, | ||
status: HttpCode.BAD_REQUEST, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
export { UserController }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,9 @@ import { UserEntity } from '~/bundles/users/user.entity.js'; | |
import { type UserModel } from '~/bundles/users/user.model.js'; | ||
import { type Repository } from '~/common/types/types.js'; | ||
|
||
import { HttpCode, HttpError, UserValidationMessage } from './enums/enums.js'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. import HttpCode, HttpError from common |
||
import { type UserDetailsModel } from './user-details.model.js'; | ||
|
||
class UserRepository implements Repository { | ||
private userModel: typeof UserModel; | ||
|
||
|
@@ -96,8 +99,43 @@ class UserRepository implements Repository { | |
} | ||
} | ||
|
||
public update(): ReturnType<Repository['update']> { | ||
return Promise.resolve(null); | ||
public async update( | ||
userId: number, | ||
updatedUserDetails: Partial<UserDetailsModel>, | ||
): Promise<UserEntity> { | ||
const trx = await this.userModel.startTransaction(); | ||
|
||
try { | ||
const user = await this.userModel.query(trx).findById(userId); | ||
|
||
if (!user) { | ||
throw new HttpError({ | ||
message: UserValidationMessage.USER_NOT_FOUND, | ||
status: HttpCode.NOT_FOUND, | ||
}); | ||
} | ||
valery-chumak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await user | ||
.$relatedQuery('userDetails', trx) | ||
.patch(updatedUserDetails); | ||
|
||
const userDetails = await user.$relatedQuery('userDetails', trx); | ||
|
||
await trx.commit(); | ||
|
||
return UserEntity.initialize({ | ||
...user, | ||
fullName: userDetails.fullName, | ||
avatarUrl: userDetails.avatarUrl, | ||
username: userDetails.username, | ||
dateOfBirth: userDetails.dateOfBirth, | ||
weight: userDetails.weight, | ||
height: userDetails.height, | ||
gender: userDetails.gender, | ||
}); | ||
} catch (error) { | ||
await trx.rollback(); | ||
throw new Error(`Error updating user details: ${error}`); | ||
} | ||
} | ||
|
||
public delete(): ReturnType<Repository['delete']> { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -3,11 +3,14 @@ import { type UserRepository } from '~/bundles/users/user.repository.js'; | |||||
import { cryptService } from '~/common/services/services.js'; | ||||||
import { type Service } from '~/common/types/types.js'; | ||||||
|
||||||
import { HttpCode, HttpError, UserValidationMessage } from './enums/enums.js'; | ||||||
import { | ||||||
type UserAuthRequestDto, | ||||||
type UserAuthResponseDto, | ||||||
type UserGetAllResponseDto, | ||||||
type UserUpdateProfileRequestDto, | ||||||
} from './types/types.js'; | ||||||
import { type UserDetailsModel } from './user-details.model.js'; | ||||||
|
||||||
class UserService implements Service { | ||||||
private userRepository: UserRepository; | ||||||
|
@@ -46,10 +49,49 @@ class UserService implements Service { | |||||
return user.toObject() as UserAuthResponseDto; | ||||||
} | ||||||
|
||||||
public update(): ReturnType<Service['update']> { | ||||||
return Promise.resolve(null); | ||||||
public async update( | ||||||
userId: number, | ||||||
userRequest: UserUpdateProfileRequestDto, | ||||||
): Promise<UserAuthResponseDto | null> { | ||||||
try { | ||||||
const existingUser = await this.userRepository.find({ | ||||||
id: userId, | ||||||
}); | ||||||
dimapopovych marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if (existingUser) { | ||||||
const updatedUserDetails: Partial<UserDetailsModel> = {}; | ||||||
for (const property of Object.keys(userRequest)) { | ||||||
const value = userRequest[property]; | ||||||
if (this.shouldUpdateProperty(value)) { | ||||||
updatedUserDetails[property] = | ||||||
property === 'weight' || property === 'height' | ||||||
? Number(userRequest[property]) | ||||||
: userRequest[property]; | ||||||
} | ||||||
} | ||||||
const updatedUser = await this.userRepository.update( | ||||||
userId, | ||||||
updatedUserDetails, | ||||||
); | ||||||
if (!updatedUser) { | ||||||
throw new HttpError({ | ||||||
message: UserValidationMessage.USER_NOT_FOUND, | ||||||
status: HttpCode.NOT_FOUND, | ||||||
}); | ||||||
} | ||||||
return updatedUser.toObject() as UserAuthResponseDto; | ||||||
} else { | ||||||
throw new HttpError({ | ||||||
message: UserValidationMessage.USER_NOT_FOUND, | ||||||
status: HttpCode.NOT_FOUND, | ||||||
}); | ||||||
} | ||||||
} catch (error) { | ||||||
throw new Error(`Error occured ${error}`); | ||||||
} | ||||||
} | ||||||
private shouldUpdateProperty(value: unknown): boolean { | ||||||
return value !== null && value !== ''; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
will this work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes) But there is an error that type 'unknown' is not assignable to type 'boolean'. So, I can make it like this |
||||||
} | ||||||
|
||||||
public delete(): ReturnType<Service['delete']> { | ||||||
return Promise.resolve(true); | ||||||
} | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
export { userAuthValidationSchema } from 'shared'; | ||
export { | ||
userAuthValidationSchema, | ||
userUpdateProfileValidationSchema, | ||
} from 'shared'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,15 @@ | ||
import { type UserUpdateProfileRequestDto } from 'shared'; | ||
|
||
type Service<T = unknown> = { | ||
find(query: Record<string, T>): Promise<T>; | ||
findAll(): Promise<{ | ||
items: T[]; | ||
}>; | ||
create(payload: unknown): Promise<T>; | ||
update(): Promise<T>; | ||
update( | ||
id: number, | ||
updatedDetails: UserUpdateProfileRequestDto, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we shouldn't set UserUpdateProfileRequestDto in global service type because we implement Service in many places. For example, for goals or workouts we need to update some data and we will get an error because in update we have to pass UserUpdateProfileRequestDto. |
||
): Promise<T | null>; | ||
delete(): Promise<boolean>; | ||
}; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { type AuthResponseDto } from 'shared'; | ||
export { type AuthResponseDto, type UserAuthResponseDto } from 'shared'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have HttpCode, HttpError in backend/src/common/http/http.ts, I guess you don't need this export here