diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6b9372e..2a98433 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,6 +10,14 @@ +## Changed Side + + + +- [ ] 💻 Backend +- [ ] 📱 Mobile App +- [ ] 🔧 Configuration + ## Type of Change diff --git a/.github/workflows/build_and_push_backend.yml b/.github/workflows/build_and_push_backend.yml new file mode 100644 index 0000000..a882279 --- /dev/null +++ b/.github/workflows/build_and_push_backend.yml @@ -0,0 +1,60 @@ +name: Build backend and push Docker Image + +on: + push: + branches: + - dev + paths: + - backend/** + pull_request: + branches: + - dev + paths: + - backend/** + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - name: Install Dart Frog + run: dart pub global activate dart_frog_cli + + - name: Create Dev Build + run: dart_frog build + + - name: Change Directory to build folder + run: cd build/ + + # - name: Overwrite file + # uses: "DamianReeves/write-file-action@master" + # with: + # path: nixpacks.toml + # write-mode: overwrite + # contents: | + # [phases.setup] + # nixpkgsArchive = 'bc901a14315f03cb02d5be6d7e4c8075cd0fe36c' + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: build/ + push: true + tags: dungngminh/server:latest diff --git a/backend/docs/index.html b/backend/docs/index.html new file mode 100644 index 0000000..d4a6181 --- /dev/null +++ b/backend/docs/index.html @@ -0,0 +1,26 @@ + + + + + + + SwaggerUI + + + +
+ + + + + \ No newline at end of file diff --git a/backend/e2e/auth_test.dart b/backend/e2e/auth_test.dart index 85c97ac..c4a8cca 100644 --- a/backend/e2e/auth_test.dart +++ b/backend/e2e/auth_test.dart @@ -91,7 +91,7 @@ void main() { ); final loginResponse = - LoginResponse.fromJson(baseResponse.data as Map); + LoginResponse.fromJson(baseResponse.result as Map); expect(response.statusCode, HttpStatus.ok); expect( diff --git a/backend/lib/common/error_message_code.dart b/backend/lib/common/error_message_code.dart new file mode 100644 index 0000000..9da4005 --- /dev/null +++ b/backend/lib/common/error_message_code.dart @@ -0,0 +1,18 @@ +class ErrorMessageCode { + ErrorMessageCode._(); + + // Base + static const unknownError = 'unknown-error'; + static const bodyEmpty = 'body-empty'; + + // Login + static const notRegisterYet = 'not-register-yet'; + static const invalidEmailOrPassword = 'invalid-email-or-password'; + + // Register + static const emailRegisterd = 'email-registerd'; + + + // Blog + static const blocNotFound = 'blog-not-found'; +} diff --git a/backend/lib/dtos/response/auth/login_response.dart b/backend/lib/dtos/response/auth/login_response.dart index 6ae7a08..4566281 100644 --- a/backend/lib/dtos/response/auth/login_response.dart +++ b/backend/lib/dtos/response/auth/login_response.dart @@ -10,7 +10,8 @@ class LoginResponse { _$LoginResponseFromJson(json); final String id; - final String token; + final String token; Map toJson() => _$LoginResponseToJson(this); } + \ No newline at end of file diff --git a/backend/lib/dtos/response/base_pagination_response.g.dart b/backend/lib/dtos/response/base_pagination_response.g.dart index 2296c27..05f9361 100644 --- a/backend/lib/dtos/response/base_pagination_response.g.dart +++ b/backend/lib/dtos/response/base_pagination_response.g.dart @@ -13,9 +13,11 @@ BasePaginationResponse _$BasePaginationResponseFromJson( json, ($checkedConvert) { final val = BasePaginationResponse( - currentPage: $checkedConvert('current_page', (v) => v as int? ?? 0), - limit: $checkedConvert('limit', (v) => v as int? ?? 0), - totalCount: $checkedConvert('total_count', (v) => v as int? ?? 0), + currentPage: + $checkedConvert('current_page', (v) => (v as num?)?.toInt() ?? 0), + limit: $checkedConvert('limit', (v) => (v as num?)?.toInt() ?? 0), + totalCount: + $checkedConvert('total_count', (v) => (v as num?)?.toInt() ?? 0), ); return val; }, diff --git a/backend/lib/dtos/response/base_response_data.dart b/backend/lib/dtos/response/base_response_data.dart index 46a872b2..cfc1ec9 100644 --- a/backend/lib/dtos/response/base_response_data.dart +++ b/backend/lib/dtos/response/base_response_data.dart @@ -13,17 +13,18 @@ part 'base_response_data.g.dart'; class BaseResponseData { const BaseResponseData({ required this.success, - this.data, + this.result, + this.errorCode, this.message = kSuccessResponseMessage, }); factory BaseResponseData.fromJson(Map json) => _$BaseResponseDataFromJson(json); - factory BaseResponseData.data(dynamic data) { + factory BaseResponseData.data(dynamic result) { return BaseResponseData( success: true, - data: data, + result: result, ); } @@ -34,28 +35,32 @@ class BaseResponseData { ); } - factory BaseResponseData.failed([String? message]) { + factory BaseResponseData.failed({String? errorCode, String? message}) { return BaseResponseData( success: false, + errorCode: errorCode, message: message ?? kFailedResponseMessage, ); } final bool success; + final String? errorCode; final String message; - final dynamic data; + final dynamic result; Map toJson() => _$BaseResponseDataToJson(this); BaseResponseData copyWith({ bool? success, + String? errorCode, String? message, - dynamic data, + dynamic result, }) { return BaseResponseData( success: success ?? this.success, + errorCode: errorCode ?? this.errorCode, message: message ?? this.message, - data: data ?? this.data, + result: result ?? this.result, ); } } @@ -79,50 +84,56 @@ class CreatedResponse extends Response { } class NotFoundResponse extends Response { - NotFoundResponse([String? message]) + NotFoundResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.notFound, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } class ConflictResponse extends Response { - ConflictResponse([String? message]) + ConflictResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.conflict, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } class UnauthorizedResponse extends Response { - UnauthorizedResponse([String? message]) + UnauthorizedResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.unauthorized, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } class BadRequestResponse extends Response { - BadRequestResponse([String? message]) + BadRequestResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.badRequest, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } class ForbiddenResponse extends Response { - ForbiddenResponse([String? message]) + ForbiddenResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.forbidden, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } -class ServerErrorResponse extends Response { - ServerErrorResponse([String? message]) +class InternalServerErrorResponse extends Response { + InternalServerErrorResponse([String? errorCode, String? message]) : super.json( statusCode: HttpStatus.internalServerError, - body: BaseResponseData.failed(message).toJson(), + body: BaseResponseData.failed(errorCode: errorCode, message: message) + .toJson(), ); } diff --git a/backend/lib/dtos/response/base_response_data.g.dart b/backend/lib/dtos/response/base_response_data.g.dart index de59495..3d4297d 100644 --- a/backend/lib/dtos/response/base_response_data.g.dart +++ b/backend/lib/dtos/response/base_response_data.g.dart @@ -13,18 +13,19 @@ BaseResponseData _$BaseResponseDataFromJson(Map json) => ($checkedConvert) { final val = BaseResponseData( success: $checkedConvert('success', (v) => v as bool), - data: $checkedConvert('data', (v) => v), + result: $checkedConvert('result', (v) => v), + errorCode: $checkedConvert('error_code', (v) => v as String?), message: $checkedConvert( 'message', (v) => v as String? ?? kSuccessResponseMessage), ); return val; }, + fieldKeyMap: const {'errorCode': 'error_code'}, ); Map _$BaseResponseDataToJson(BaseResponseData instance) { final val = { 'success': instance.success, - 'message': instance.message, }; void writeNotNull(String key, dynamic value) { @@ -33,6 +34,8 @@ Map _$BaseResponseDataToJson(BaseResponseData instance) { } } - writeNotNull('data', instance.data); + writeNotNull('error_code', instance.errorCode); + val['message'] = instance.message; + writeNotNull('result', instance.result); return val; } diff --git a/backend/lib/dtos/response/blogs/get_blog_response.g.dart b/backend/lib/dtos/response/blogs/get_blog_response.g.dart index 7281c22..b6cd1a9 100644 --- a/backend/lib/dtos/response/blogs/get_blog_response.g.dart +++ b/backend/lib/dtos/response/blogs/get_blog_response.g.dart @@ -69,8 +69,8 @@ UserOfGetBlogResponse _$UserOfGetBlogResponseFromJson( id: $checkedConvert('id', (v) => v as String), fullName: $checkedConvert('full_name', (v) => v as String), email: $checkedConvert('email', (v) => v as String), - following: $checkedConvert('following', (v) => v as int), - follower: $checkedConvert('follower', (v) => v as int), + following: $checkedConvert('following', (v) => (v as num).toInt()), + follower: $checkedConvert('follower', (v) => (v as num).toInt()), avatarUrl: $checkedConvert('avatar_url', (v) => v as String?), ); return val; diff --git a/backend/lib/dtos/response/users/followers/get_user_profile_response.dart b/backend/lib/dtos/response/users/followers/get_user_profile_response.dart index eb85630..a9da522 100644 --- a/backend/lib/dtos/response/users/followers/get_user_profile_response.dart +++ b/backend/lib/dtos/response/users/followers/get_user_profile_response.dart @@ -1,5 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:very_good_blog_app_backend/models/following_follower.dart'; +import 'package:very_good_blog_app_backend/models/user.dart'; part 'get_user_profile_response.g.dart'; @@ -15,12 +16,12 @@ class GetUserFollowerResponse { factory GetUserFollowerResponse.fromJson(Map json) => _$GetUserFollowerResponseFromJson(json); - factory GetUserFollowerResponse.fromView(FollowingFollowerView view) { + factory GetUserFollowerResponse.fromView(UserView follower) { return GetUserFollowerResponse( - id: view.follower.id, - fullName: view.follower.fullName, - email: view.follower.email, - avatarUrl: view.follower.avatarUrl, + id: follower.id, + fullName: follower.fullName, + email: follower.email, + avatarUrl: follower.avatarUrl, ); } diff --git a/backend/lib/dtos/response/users/followings/get_user_following_response.dart b/backend/lib/dtos/response/users/followings/get_user_following_response.dart index 00518e3..7d55594 100644 --- a/backend/lib/dtos/response/users/followings/get_user_following_response.dart +++ b/backend/lib/dtos/response/users/followings/get_user_following_response.dart @@ -1,5 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:very_good_blog_app_backend/models/following_follower.dart'; +import 'package:very_good_blog_app_backend/models/user.dart'; part 'get_user_following_response.g.dart'; @@ -15,12 +15,12 @@ class GetUserFollowingResponse { factory GetUserFollowingResponse.fromJson(Map json) => _$GetUserFollowingResponseFromJson(json); - factory GetUserFollowingResponse.fromView(FollowingFollowerView view) { + factory GetUserFollowingResponse.fromView(UserView following) { return GetUserFollowingResponse( - id: view.following.id, - fullName: view.following.fullName, - email: view.following.email, - avatarUrl: view.following.avatarUrl, + id: following.id, + fullName: following.fullName, + email: following.email, + avatarUrl: following.avatarUrl, ); } diff --git a/backend/lib/dtos/response/users/profiles/get_user_profile_response.g.dart b/backend/lib/dtos/response/users/profiles/get_user_profile_response.g.dart index b4a7b62..7938563 100644 --- a/backend/lib/dtos/response/users/profiles/get_user_profile_response.g.dart +++ b/backend/lib/dtos/response/users/profiles/get_user_profile_response.g.dart @@ -16,8 +16,8 @@ GetUserProfileResponse _$GetUserProfileResponseFromJson( id: $checkedConvert('id', (v) => v as String), fullName: $checkedConvert('full_name', (v) => v as String), email: $checkedConvert('email', (v) => v as String), - following: $checkedConvert('following', (v) => v as int), - follower: $checkedConvert('follower', (v) => v as int), + following: $checkedConvert('following', (v) => (v as num).toInt()), + follower: $checkedConvert('follower', (v) => (v as num).toInt()), avatarUrl: $checkedConvert('avatar_url', (v) => v as String?), ); return val; diff --git a/backend/lib/models/blog.schema.dart b/backend/lib/models/blog.schema.dart index 905513c..dd0daa2 100644 --- a/backend/lib/models/blog.schema.dart +++ b/backend/lib/models/blog.schema.dart @@ -2,7 +2,7 @@ part of 'blog.dart'; -extension BlogRepositories on Database { +extension BlogRepositories on Session { BlogRepository get blogs => BlogRepository._(this); } @@ -12,7 +12,7 @@ abstract class BlogRepository ModelRepositoryInsert, ModelRepositoryUpdate, ModelRepositoryDelete { - factory BlogRepository._(Database db) = _BlogRepository; + factory BlogRepository._(Session db) = _BlogRepository; Future queryBlog(String id); Future> queryBlogs([QueryParams? params]); @@ -40,17 +40,18 @@ class _BlogRepository extends BaseRepository Future insert(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'INSERT INTO "blogs" ( "id", "title", "content", "image_url", "category", "created_at", "updated_at", "creator_id", "is_deleted" )\n' - 'VALUES ${requests.map((r) => '( ${values.add(r.id)}:text, ${values.add(r.title)}:text, ${values.add(r.content)}:text, ${values.add(r.imageUrl)}:text, ${values.add(EnumTypeConverter([ - BlogCategory.business, - BlogCategory.technology, - BlogCategory.fashion, - BlogCategory.travel, - BlogCategory.food, - BlogCategory.education - ]).tryEncode(r.category))}:text, ${values.add(r.createdAt)}:timestamp, ${values.add(r.updatedAt)}:timestamp, ${values.add(r.creatorId)}:text, ${values.add(r.isDeleted)}:boolean )').join(', ')}\n', - values.values, + await db.execute( + Sql.named( + 'INSERT INTO "blogs" ( "id", "title", "content", "image_url", "category", "created_at", "updated_at", "creator_id", "is_deleted" )\n' + 'VALUES ${requests.map((r) => '( ${values.add(r.id)}:text, ${values.add(r.title)}:text, ${values.add(r.content)}:text, ${values.add(r.imageUrl)}:text, ${values.add(EnumTypeConverter([ + BlogCategory.business, + BlogCategory.technology, + BlogCategory.fashion, + BlogCategory.travel, + BlogCategory.food, + BlogCategory.education + ]).tryEncode(r.category))}:text, ${values.add(r.createdAt)}:timestamp, ${values.add(r.updatedAt)}:timestamp, ${values.add(r.creatorId)}:text, ${values.add(r.isDeleted)}:boolean )').join(', ')}\n'), + parameters: values.values, ); } @@ -58,20 +59,20 @@ class _BlogRepository extends BaseRepository Future update(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'UPDATE "blogs"\n' - 'SET "title" = COALESCE(UPDATED."title", "blogs"."title"), "content" = COALESCE(UPDATED."content", "blogs"."content"), "image_url" = COALESCE(UPDATED."image_url", "blogs"."image_url"), "category" = COALESCE(UPDATED."category", "blogs"."category"), "created_at" = COALESCE(UPDATED."created_at", "blogs"."created_at"), "updated_at" = COALESCE(UPDATED."updated_at", "blogs"."updated_at"), "creator_id" = COALESCE(UPDATED."creator_id", "blogs"."creator_id"), "is_deleted" = COALESCE(UPDATED."is_deleted", "blogs"."is_deleted")\n' - 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.id)}:text::text, ${values.add(r.title)}:text::text, ${values.add(r.content)}:text::text, ${values.add(r.imageUrl)}:text::text, ${values.add(EnumTypeConverter([ - BlogCategory.business, - BlogCategory.technology, - BlogCategory.fashion, - BlogCategory.travel, - BlogCategory.food, - BlogCategory.education - ]).tryEncode(r.category))}:text::text, ${values.add(r.createdAt)}:timestamp::timestamp, ${values.add(r.updatedAt)}:timestamp::timestamp, ${values.add(r.creatorId)}:text::text, ${values.add(r.isDeleted)}:boolean::boolean )').join(', ')} )\n' - 'AS UPDATED("id", "title", "content", "image_url", "category", "created_at", "updated_at", "creator_id", "is_deleted")\n' - 'WHERE "blogs"."id" = UPDATED."id"', - values.values, + await db.execute( + Sql.named('UPDATE "blogs"\n' + 'SET "title" = COALESCE(UPDATED."title", "blogs"."title"), "content" = COALESCE(UPDATED."content", "blogs"."content"), "image_url" = COALESCE(UPDATED."image_url", "blogs"."image_url"), "category" = COALESCE(UPDATED."category", "blogs"."category"), "created_at" = COALESCE(UPDATED."created_at", "blogs"."created_at"), "updated_at" = COALESCE(UPDATED."updated_at", "blogs"."updated_at"), "creator_id" = COALESCE(UPDATED."creator_id", "blogs"."creator_id"), "is_deleted" = COALESCE(UPDATED."is_deleted", "blogs"."is_deleted")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.id)}:text::text, ${values.add(r.title)}:text::text, ${values.add(r.content)}:text::text, ${values.add(r.imageUrl)}:text::text, ${values.add(EnumTypeConverter([ + BlogCategory.business, + BlogCategory.technology, + BlogCategory.fashion, + BlogCategory.travel, + BlogCategory.food, + BlogCategory.education + ]).tryEncode(r.category))}:text::text, ${values.add(r.createdAt)}:timestamp::timestamp, ${values.add(r.updatedAt)}:timestamp::timestamp, ${values.add(r.creatorId)}:text::text, ${values.add(r.isDeleted)}:boolean::boolean )').join(', ')} )\n' + 'AS UPDATED("id", "title", "content", "image_url", "category", "created_at", "updated_at", "creator_id", "is_deleted")\n' + 'WHERE "blogs"."id" = UPDATED."id"'), + parameters: values.values, ); } } diff --git a/backend/lib/models/favorite_blogs_users.schema.dart b/backend/lib/models/favorite_blogs_users.schema.dart index 88a9343..6fb0977 100644 --- a/backend/lib/models/favorite_blogs_users.schema.dart +++ b/backend/lib/models/favorite_blogs_users.schema.dart @@ -2,7 +2,7 @@ part of 'favorite_blogs_users.dart'; -extension FavoriteBlogsUsersRepositories on Database { +extension FavoriteBlogsUsersRepositories on Session { FavoriteBlogsUsersRepository get favoriteBlogsUserses => FavoriteBlogsUsersRepository._(this); } @@ -11,7 +11,7 @@ abstract class FavoriteBlogsUsersRepository ModelRepository, ModelRepositoryInsert, ModelRepositoryUpdate { - factory FavoriteBlogsUsersRepository._(Database db) = _FavoriteBlogsUsersRepository; + factory FavoriteBlogsUsersRepository._(Session db) = _FavoriteBlogsUsersRepository; Future> queryFavoriteBlogsUserses([QueryParams? params]); } @@ -32,10 +32,10 @@ class _FavoriteBlogsUsersRepository extends BaseRepository Future insert(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'INSERT INTO "favorite_blogs_userses" ( "blog_id", "user_id" )\n' - 'VALUES ${requests.map((r) => '( ${values.add(r.blogId)}:text, ${values.add(r.userId)}:text )').join(', ')}\n', - values.values, + await db.execute( + Sql.named('INSERT INTO "favorite_blogs_userses" ( "blog_id", "user_id" )\n' + 'VALUES ${requests.map((r) => '( ${values.add(r.blogId)}:text, ${values.add(r.userId)}:text )').join(', ')}\n'), + parameters: values.values, ); } @@ -43,13 +43,13 @@ class _FavoriteBlogsUsersRepository extends BaseRepository Future update(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'UPDATE "favorite_blogs_userses"\n' - 'SET \n' - 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.blogId)}:text::text, ${values.add(r.userId)}:text::text )').join(', ')} )\n' - 'AS UPDATED("blog_id", "user_id")\n' - 'WHERE "favorite_blogs_userses"."blog_id" = UPDATED."blog_id" AND "favorite_blogs_userses"."user_id" = UPDATED."user_id"', - values.values, + await db.execute( + Sql.named('UPDATE "favorite_blogs_userses"\n' + 'SET \n' + 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.blogId)}:text::text, ${values.add(r.userId)}:text::text )').join(', ')} )\n' + 'AS UPDATED("blog_id", "user_id")\n' + 'WHERE "favorite_blogs_userses"."blog_id" = UPDATED."blog_id" AND "favorite_blogs_userses"."user_id" = UPDATED."user_id"'), + parameters: values.values, ); } } diff --git a/backend/lib/models/following_follower.dart b/backend/lib/models/following_follower.dart index 7ce4f0e..60b2b66 100644 --- a/backend/lib/models/following_follower.dart +++ b/backend/lib/models/following_follower.dart @@ -1,10 +1,9 @@ import 'package:stormberry/stormberry.dart'; -import 'package:very_good_blog_app_backend/models/user.dart'; part 'following_follower.schema.dart'; @Model() abstract class FollowingFollower { - User get following; - User get follower; + String get followingId; + String get followerId; } diff --git a/backend/lib/models/following_follower.schema.dart b/backend/lib/models/following_follower.schema.dart index b268b3b..b6bdbc0 100644 --- a/backend/lib/models/following_follower.schema.dart +++ b/backend/lib/models/following_follower.schema.dart @@ -2,7 +2,7 @@ part of 'following_follower.dart'; -extension FollowingFollowerRepositories on Database { +extension FollowingFollowerRepositories on Session { FollowingFollowerRepository get followingFollowers => FollowingFollowerRepository._(this); } @@ -11,7 +11,7 @@ abstract class FollowingFollowerRepository ModelRepository, ModelRepositoryInsert, ModelRepositoryUpdate { - factory FollowingFollowerRepository._(Database db) = _FollowingFollowerRepository; + factory FollowingFollowerRepository._(Session db) = _FollowingFollowerRepository; Future> queryFollowingFollowers([QueryParams? params]); } @@ -32,10 +32,10 @@ class _FollowingFollowerRepository extends BaseRepository Future insert(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'INSERT INTO "following_followers" ( "following_id", "follower_id" )\n' - 'VALUES ${requests.map((r) => '( ${values.add(r.followingId)}:text, ${values.add(r.followerId)}:text )').join(', ')}\n', - values.values, + await db.execute( + Sql.named('INSERT INTO "following_followers" ( "following_id", "follower_id" )\n' + 'VALUES ${requests.map((r) => '( ${values.add(r.followingId)}:text, ${values.add(r.followerId)}:text )').join(', ')}\n'), + parameters: values.values, ); } @@ -43,13 +43,13 @@ class _FollowingFollowerRepository extends BaseRepository Future update(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'UPDATE "following_followers"\n' - 'SET \n' - 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.followingId)}:text::text, ${values.add(r.followerId)}:text::text )').join(', ')} )\n' - 'AS UPDATED("following_id", "follower_id")\n' - 'WHERE "following_followers"."following_id" = UPDATED."following_id" AND "following_followers"."follower_id" = UPDATED."follower_id"', - values.values, + await db.execute( + Sql.named('UPDATE "following_followers"\n' + 'SET "following_id" = COALESCE(UPDATED."following_id", "following_followers"."following_id"), "follower_id" = COALESCE(UPDATED."follower_id", "following_followers"."follower_id")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.followingId)}:text::text, ${values.add(r.followerId)}:text::text )').join(', ')} )\n' + 'AS UPDATED("following_id", "follower_id")\n' + 'WHERE '), + parameters: values.values, ); } } @@ -76,29 +76,23 @@ class FollowingFollowerUpdateRequest { class FollowingFollowerViewQueryable extends ViewQueryable { @override - String get query => - 'SELECT "following_followers".*, row_to_json("following".*) as "following", row_to_json("follower".*) as "follower"' - 'FROM "following_followers"' - 'LEFT JOIN (${UserViewQueryable().query}) "following"' - 'ON "following_followers"."following_id" = "following"."id"' - 'LEFT JOIN (${UserViewQueryable().query}) "follower"' - 'ON "following_followers"."follower_id" = "follower"."id"'; + String get query => 'SELECT "following_followers".*' + 'FROM "following_followers"'; @override String get tableAlias => 'following_followers'; @override FollowingFollowerView decode(TypedMap map) => FollowingFollowerView( - following: map.get('following', UserViewQueryable().decoder), - follower: map.get('follower', UserViewQueryable().decoder)); + followingId: map.get('following_id'), followerId: map.get('follower_id')); } class FollowingFollowerView { FollowingFollowerView({ - required this.following, - required this.follower, + required this.followingId, + required this.followerId, }); - final UserView following; - final UserView follower; + final String followingId; + final String followerId; } diff --git a/backend/lib/models/user.schema.dart b/backend/lib/models/user.schema.dart index c6df0ea..119a331 100644 --- a/backend/lib/models/user.schema.dart +++ b/backend/lib/models/user.schema.dart @@ -2,7 +2,7 @@ part of 'user.dart'; -extension UserRepositories on Database { +extension UserRepositories on Session { UserRepository get users => UserRepository._(this); } @@ -12,7 +12,7 @@ abstract class UserRepository ModelRepositoryInsert, ModelRepositoryUpdate, ModelRepositoryDelete { - factory UserRepository._(Database db) = _UserRepository; + factory UserRepository._(Session db) = _UserRepository; Future queryUser(String id); Future> queryUsers([QueryParams? params]); @@ -40,10 +40,11 @@ class _UserRepository extends BaseRepository Future insert(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'INSERT INTO "users" ( "id", "full_name", "email", "password", "avatar_url", "following", "follower" )\n' - 'VALUES ${requests.map((r) => '( ${values.add(r.id)}:text, ${values.add(r.fullName)}:text, ${values.add(r.email)}:text, ${values.add(r.password)}:text, ${values.add(r.avatarUrl)}:text, ${values.add(r.following)}:int8, ${values.add(r.follower)}:int8 )').join(', ')}\n', - values.values, + await db.execute( + Sql.named( + 'INSERT INTO "users" ( "id", "full_name", "email", "password", "avatar_url", "following", "follower" )\n' + 'VALUES ${requests.map((r) => '( ${values.add(r.id)}:text, ${values.add(r.fullName)}:text, ${values.add(r.email)}:text, ${values.add(r.password)}:text, ${values.add(r.avatarUrl)}:text, ${values.add(r.following)}:int8, ${values.add(r.follower)}:int8 )').join(', ')}\n'), + parameters: values.values, ); } @@ -51,13 +52,13 @@ class _UserRepository extends BaseRepository Future update(List requests) async { if (requests.isEmpty) return; var values = QueryValues(); - await db.query( - 'UPDATE "users"\n' - 'SET "full_name" = COALESCE(UPDATED."full_name", "users"."full_name"), "email" = COALESCE(UPDATED."email", "users"."email"), "password" = COALESCE(UPDATED."password", "users"."password"), "avatar_url" = COALESCE(UPDATED."avatar_url", "users"."avatar_url"), "following" = COALESCE(UPDATED."following", "users"."following"), "follower" = COALESCE(UPDATED."follower", "users"."follower")\n' - 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.id)}:text::text, ${values.add(r.fullName)}:text::text, ${values.add(r.email)}:text::text, ${values.add(r.password)}:text::text, ${values.add(r.avatarUrl)}:text::text, ${values.add(r.following)}:int8::int8, ${values.add(r.follower)}:int8::int8 )').join(', ')} )\n' - 'AS UPDATED("id", "full_name", "email", "password", "avatar_url", "following", "follower")\n' - 'WHERE "users"."id" = UPDATED."id"', - values.values, + await db.execute( + Sql.named('UPDATE "users"\n' + 'SET "full_name" = COALESCE(UPDATED."full_name", "users"."full_name"), "email" = COALESCE(UPDATED."email", "users"."email"), "password" = COALESCE(UPDATED."password", "users"."password"), "avatar_url" = COALESCE(UPDATED."avatar_url", "users"."avatar_url"), "following" = COALESCE(UPDATED."following", "users"."following"), "follower" = COALESCE(UPDATED."follower", "users"."follower")\n' + 'FROM ( VALUES ${requests.map((r) => '( ${values.add(r.id)}:text::text, ${values.add(r.fullName)}:text::text, ${values.add(r.email)}:text::text, ${values.add(r.password)}:text::text, ${values.add(r.avatarUrl)}:text::text, ${values.add(r.following)}:int8::int8, ${values.add(r.follower)}:int8::int8 )').join(', ')} )\n' + 'AS UPDATED("id", "full_name", "email", "password", "avatar_url", "following", "follower")\n' + 'WHERE "users"."id" = UPDATED."id"'), + parameters: values.values, ); } } diff --git a/backend/main.dart b/backend/main.dart index fc9aa77..6569281 100644 --- a/backend/main.dart +++ b/backend/main.dart @@ -3,5 +3,13 @@ import 'dart:io'; import 'package:dart_frog/dart_frog.dart'; Future run(Handler handler, InternetAddress ip, int port) { - return serve(handler, ip, port, poweredByHeader: null); + final flavor = Platform.environment['FLAVOR']; + if (flavor == 'DEV') { + return serve(handler, ip, port, poweredByHeader: null); + } + const customStaticDocumentPath = 'docs'; + final cascade = Cascade() + .add(createStaticFileHandler(path: customStaticDocumentPath)) + .add(handler); + return serve(cascade.handler, ip, port, poweredByHeader: null); } diff --git a/backend/public/openapi.json b/backend/public/openapi.json index 27c189f..635cba0 100644 --- a/backend/public/openapi.json +++ b/backend/public/openapi.json @@ -1,19 +1,12 @@ { "openapi": "3.0.3", "info": { - "title": "A sample API", - "description": "A sample API", - "termsOfService": "https://very-good-blog-app.up.railway.app", + "title": "Very Good Blog App Dart API Document", "contact": { - "name": "none", - "url": "http://localhost", - "email": "none@api.com" + "name": "dungngminh", + "email": "ngminhdung1311@gmail.com" }, - "license": { - "name": "", - "url": "" - }, - "version": "0.0.0" + "version": "1.0.0" }, "externalDocs": { "description": "", @@ -159,6 +152,16 @@ "schema": { "type": "string" } + }, + { + "name": "search", + "in": "query", + "required": false, + "deprecated": false, + "allowEmptyValue": false, + "schema": { + "type": "string" + } } ], "security": [] @@ -205,6 +208,16 @@ "schema": { "type": "string" } + }, + { + "name": "search", + "in": "query", + "required": false, + "deprecated": false, + "allowEmptyValue": false, + "schema": { + "type": "string" + } } ], "security": [] diff --git a/backend/pubspec.yaml b/backend/pubspec.yaml index a1ed922..8f281f9 100644 --- a/backend/pubspec.yaml +++ b/backend/pubspec.yaml @@ -4,26 +4,26 @@ version: 1.0.0+1 publish_to: none environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ">=3.3.0 <4.0.0" dependencies: cloudinary: ^1.2.0 crypto: ^3.0.3 - dart_frog: ^1.0.0 + dart_frog: ^1.1.0 dart_frog_auth: ^1.1.0 - dart_jsonwebtoken: ^2.11.0 + dart_jsonwebtoken: ^2.14.0 dartx: ^1.2.0 equatable: ^2.0.5 - http: ^1.1.0 - json_annotation: ^4.8.1 + http: ^1.2.1 + json_annotation: ^4.9.0 shelf_enforces_ssl: ^1.2.1 - stormberry: ^0.13.1 - string_validator: ^1.0.0 - uuid: ^3.0.7 + stormberry: ^0.14.0 + string_validator: ^1.0.2 + uuid: ^4.4.0 dev_dependencies: - build_runner: ^2.4.6 - json_serializable: ^6.7.1 - mocktail: ^0.3.0 - test: ^1.19.2 - very_good_analysis: ^5.0.0 + build_runner: ^2.4.9 + json_serializable: ^6.8.0 + mocktail: ^1.0.3 + test: ^1.25.4 + very_good_analysis: ^5.1.0 diff --git a/backend/routes/_middleware.dart b/backend/routes/_middleware.dart index c6a0711..07affdb 100644 --- a/backend/routes/_middleware.dart +++ b/backend/routes/_middleware.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cloudinary/cloudinary.dart'; import 'package:dart_frog/dart_frog.dart'; +import 'package:dartx/dartx.dart'; import 'package:stormberry/stormberry.dart'; import 'package:very_good_blog_app_backend/models/user.dart'; import 'package:very_good_blog_app_backend/util/jwt_handler.dart'; @@ -10,15 +11,15 @@ final db = Database( host: Platform.environment['PGHOST'], port: int.tryParse(Platform.environment['PGPORT'] ?? '5432'), database: Platform.environment['PGDATABASE'], - user: Platform.environment['PGUSER'], + username: Platform.environment['PGUSER'], password: Platform.environment['PGPASSWORD'], useSSL: false, ); final cloudinary = Cloudinary.signedConfig( - apiKey: Platform.environment['CLOUDINARY_APIKEY'] ?? '', - apiSecret: Platform.environment['CLOUDINARY_APISECRET'] ?? '', - cloudName: Platform.environment['CLOUDINARY_CLOUDNAME'] ?? '', + apiKey: Platform.environment['CLOUDINARY_APIKEY'].orEmpty(), + apiSecret: Platform.environment['CLOUDINARY_APISECRET'].orEmpty(), + cloudName: Platform.environment['CLOUDINARY_CLOUDNAME'].orEmpty(), ); Handler middleware(Handler handler) { diff --git a/backend/routes/api/auth/login/index.dart b/backend/routes/api/auth/login/index.dart index d067b1b..f4570d3 100644 --- a/backend/routes/api/auth/login/index.dart +++ b/backend/routes/api/auth/login/index.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:dart_frog/dart_frog.dart'; import 'package:stormberry/stormberry.dart'; -import 'package:string_validator/string_validator.dart'; +import 'package:very_good_blog_app_backend/common/error_message_code.dart'; import 'package:very_good_blog_app_backend/common/extensions/hash_extension.dart'; import 'package:very_good_blog_app_backend/common/extensions/json_ext.dart'; import 'package:very_good_blog_app_backend/dtos/request/auth/login_request.dart'; @@ -24,13 +24,9 @@ Future _onLoginPostRequest(RequestContext context) async { final body = await context.request.body(); - if (body.isEmpty) return BadRequestResponse(); + if (body.isEmpty) return BadRequestResponse(ErrorMessageCode.bodyEmpty); final request = LoginRequest.fromJson(body.asJson()); - if (!isEmail(request.email)) { - return BadRequestResponse('Email format is wrong, please check again'); - } - return db.users .queryUsers( QueryParams( @@ -41,9 +37,10 @@ Future _onLoginPostRequest(RequestContext context) async { .then((users) { final user = users.firstOrNull; return user == null - ? BadRequestResponse('User is not registered') + ? BadRequestResponse(ErrorMessageCode.notRegisterYet) : OkResponse( LoginResponse(id: user.id, token: createJwt(user.id)).toJson(), ); - }).onError((e, _) => ServerErrorResponse(e.toString())); + }).onError( + (e, _) => InternalServerErrorResponse(ErrorMessageCode.unknownError)); } diff --git a/backend/routes/api/auth/register/index.dart b/backend/routes/api/auth/register/index.dart index dd82c9c..6900d54 100644 --- a/backend/routes/api/auth/register/index.dart +++ b/backend/routes/api/auth/register/index.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:dart_frog/dart_frog.dart'; import 'package:stormberry/stormberry.dart'; -import 'package:string_validator/string_validator.dart'; import 'package:uuid/uuid.dart'; +import 'package:very_good_blog_app_backend/common/error_message_code.dart'; import 'package:very_good_blog_app_backend/common/extensions/hash_extension.dart'; import 'package:very_good_blog_app_backend/common/extensions/json_ext.dart'; import 'package:very_good_blog_app_backend/dtos/request/auth/register_request.dart'; @@ -23,18 +23,10 @@ Future _onRegisterPostRequest(RequestContext context) async { final body = await context.request.body(); - if (body.isEmpty) return BadRequestResponse(); + if (body.isEmpty) return BadRequestResponse(ErrorMessageCode.bodyEmpty); final request = RegisterRequest.fromJson(body.asJson()); - if (request.password != request.confirmationPassword) { - return BadRequestResponse('Confirmation password not match'); - } - - if (!isEmail(request.email)) { - return BadRequestResponse('Email format is wrong, please check again'); - } - final users = await db.users.queryUsers( QueryParams( where: 'email=@email', @@ -44,20 +36,22 @@ Future _onRegisterPostRequest(RequestContext context) async { ), ); if (users.isNotEmpty) { - return ConflictResponse('This email was registered'); + return ConflictResponse(ErrorMessageCode.emailRegisterd); } - return db.users - .insertOne( - UserInsertRequest( - email: request.email, - follower: 0, - following: 0, - fullName: request.fullName, - id: const Uuid().v4(), - password: request.password.hashValue, - ), - ) + return db.users + .insertOne( + UserInsertRequest( + email: request.email, + follower: 0, + following: 0, + fullName: request.fullName, + id: const Uuid().v4(), + password: request.password.hashValue, + ), + ) .then((_) => CreatedResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError( + (e, st) => InternalServerErrorResponse(ErrorMessageCode.unknownError), + ); } diff --git a/backend/routes/api/blogs/[id]/index.dart b/backend/routes/api/blogs/[id]/index.dart index c89fe54..34146b0 100644 --- a/backend/routes/api/blogs/[id]/index.dart +++ b/backend/routes/api/blogs/[id]/index.dart @@ -1,6 +1,7 @@ import 'package:dart_frog/dart_frog.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stormberry/stormberry.dart'; +import 'package:very_good_blog_app_backend/common/error_message_code.dart'; import 'package:very_good_blog_app_backend/common/extensions/header_extesion.dart'; import 'package:very_good_blog_app_backend/common/extensions/json_ext.dart'; import 'package:very_good_blog_app_backend/dtos/request/blogs/edit_blog_request.dart'; @@ -52,7 +53,7 @@ Future _onBlogsGetRequest(RequestContext context, String id) async { .toJson(), ); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } @@ -61,7 +62,7 @@ Future _onBlogsPatchRequest(RequestContext context, String id) async { final user = context.read(); try { final body = await context.request.body(); - if (body.isEmpty) return BadRequestResponse(); + if (body.isEmpty) return BadRequestResponse(ErrorMessageCode.bodyEmpty); final request = EditBlogRequest.fromJson(body.asJson()); final blog = await db.blogs.queryBlog(id); if (blog == null) return NotFoundResponse('Blog not found'); @@ -82,7 +83,7 @@ Future _onBlogsPatchRequest(RequestContext context, String id) async { } on CheckedFromJsonException catch (e) { return BadRequestResponse(e.message); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } @@ -101,6 +102,6 @@ Future _onBlogsDeleteRequest( await db.blogs.deleteOne(id); return OkResponse(); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } diff --git a/backend/routes/api/blogs/index.dart b/backend/routes/api/blogs/index.dart index 58cd7b0..76dcd55 100644 --- a/backend/routes/api/blogs/index.dart +++ b/backend/routes/api/blogs/index.dart @@ -1,10 +1,10 @@ import 'package:dart_frog/dart_frog.dart'; +import 'package:dartx/dartx.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stormberry/stormberry.dart'; import 'package:uuid/uuid.dart'; import 'package:very_good_blog_app_backend/common/extensions/json_ext.dart'; import 'package:very_good_blog_app_backend/dtos/request/blogs/create_blog_request.dart'; -import 'package:very_good_blog_app_backend/dtos/response/base_pagination_response.dart'; import 'package:very_good_blog_app_backend/dtos/response/base_response_data.dart'; import 'package:very_good_blog_app_backend/dtos/response/blogs/get_blog_response.dart'; import 'package:very_good_blog_app_backend/models/blog.dart'; @@ -13,6 +13,7 @@ import 'package:very_good_blog_app_backend/models/user.dart'; /// @Allow(GET, POST) /// @Query(limit) /// @Query(page) +/// @Query(search) Future onRequest(RequestContext context) { return switch (context.request.method) { HttpMethod.get => _onBlogsGetRequest(context), @@ -24,8 +25,8 @@ Future onRequest(RequestContext context) { Future _onBlogsGetRequest(RequestContext context) async { final db = context.read(); final queryParams = context.request.uri.queryParameters; - final limit = int.tryParse(queryParams['limit'] ?? '') ?? 20; - final currentPage = int.tryParse(queryParams['page'] ?? '') ?? 1; + final limit = int.tryParse(queryParams['limit'].orEmpty()) ?? 20; + final currentPage = int.tryParse(queryParams['page'].orEmpty()) ?? 1; try { final results = await db.blogs.queryBlogs( @@ -35,19 +36,9 @@ Future _onBlogsGetRequest(RequestContext context) async { ), ); final blogs = results.map(GetBlogResponse.fromView); - final pagination = BasePaginationResponse( - currentPage: currentPage, - limit: limit, - totalCount: blogs.length, - ); - return OkResponse( - { - 'blogs': blogs.map((e) => e.toJson()).toList(), - 'pagination': pagination.toJson(), - }, - ); + return OkResponse(blogs.map((e) => e.toJson()).toList()); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } @@ -78,6 +69,6 @@ Future _onBlogsPostRequest(RequestContext context) async { } on CheckedFromJsonException catch (e) { return BadRequestResponse(e.message); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } diff --git a/backend/routes/api/favorites/index.dart b/backend/routes/api/favorites/index.dart index 3b4a1c2..e15df16 100644 --- a/backend/routes/api/favorites/index.dart +++ b/backend/routes/api/favorites/index.dart @@ -29,7 +29,7 @@ Future _onFavoritesGetRequest(RequestContext context) { ) .then((r) => r.map(GetUserFavoriteBlogResponse.fromView).toList()) .then((res) => OkResponse(res.map((e) => e.toJson()).toList())) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } Future _onFavoritesPostRequest(RequestContext context) async { @@ -72,16 +72,16 @@ Future _onFavoritesPostRequest(RequestContext context) async { ), ) .then((_) => OkResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } return db - .query( + .execute( 'DELETE FROM favorite_blogs_userses ' 'WHERE blog_id=@blogId AND user_id=@userId', - {'blogId': request.blogId, 'userId': userView.id}, + parameters: {'blogId': request.blogId, 'userId': userView.id}, ) .then((_) => OkResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } on CheckedFromJsonException catch (e) { return BadRequestResponse(e.message); } diff --git a/backend/routes/api/followings/index.dart b/backend/routes/api/followings/index.dart index 0309c99..cb98b4b 100644 --- a/backend/routes/api/followings/index.dart +++ b/backend/routes/api/followings/index.dart @@ -42,13 +42,13 @@ Future _onFollowingPost(RequestContext context) async { .firstOrNull; if (existFollowing != null) { return db - .query( + .execute( 'DELETE FROM following_followers ' 'WHERE following_id=@followingId AND follower_id=@followerId', - {'followingId': userView.id, 'userId': userView.id}, + parameters: {'followingId': userView.id, 'userId': userView.id}, ) .then((_) => OkResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } return db.followingFollowers .insertOne( @@ -58,10 +58,10 @@ Future _onFollowingPost(RequestContext context) async { ), ) .then((_) => OkResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } on CheckedFromJsonException catch (e) { return BadRequestResponse(e.message); } catch (e) { - return ServerErrorResponse(e.toString()); + return InternalServerErrorResponse(e.toString()); } } diff --git a/backend/routes/api/users/[id]/blogs.dart b/backend/routes/api/users/[id]/blogs.dart index 183e189..80d9442 100644 --- a/backend/routes/api/users/[id]/blogs.dart +++ b/backend/routes/api/users/[id]/blogs.dart @@ -19,5 +19,5 @@ Future _onUsersByIdBlogsGet(RequestContext context, String id) { .queryBlogs(QueryParams(where: 'creator_id=@id', values: {'id': id})) .then((views) => views.map(GetUserBlogResponse.fromView)) .then((res) => OkResponse(res.map((e) => e.toJson()).toList())) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } diff --git a/backend/routes/api/users/[id]/followers/index.dart b/backend/routes/api/users/[id]/followers/index.dart index 16224be..7fadc5b 100644 --- a/backend/routes/api/users/[id]/followers/index.dart +++ b/backend/routes/api/users/[id]/followers/index.dart @@ -3,6 +3,7 @@ import 'package:stormberry/stormberry.dart'; import 'package:very_good_blog_app_backend/dtos/response/base_response_data.dart'; import 'package:very_good_blog_app_backend/dtos/response/users/followers/get_user_profile_response.dart'; import 'package:very_good_blog_app_backend/models/following_follower.dart'; +import 'package:very_good_blog_app_backend/models/user.dart'; /// @Allow(GET) Future onRequest(RequestContext context, String id) { @@ -15,17 +16,28 @@ Future onRequest(RequestContext context, String id) { Future _onFollowersByIdGetRequest( RequestContext context, String id, -) { - return context - .read() - .followingFollowers +) async { + final database = context.read(); + + return database.followingFollowers .queryFollowingFollowers( QueryParams( where: 'follower_id = @id', values: {'id': id}, ), ) - .then((result) => result.map(GetUserFollowerResponse.fromView)) + .then( + (followingFollowerViews) async { + final followers = []; + for (final view in followingFollowerViews) { + final follower = await database.users.queryUser(view.followerId); + if (follower == null) continue; + followers.add(follower); + } + return followers; + }, + ) + .then((followers) => followers.map(GetUserFollowerResponse.fromView)) .then((res) => OkResponse(res.map((e) => e.toJson()).toList())) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } diff --git a/backend/routes/api/users/[id]/followings/index.dart b/backend/routes/api/users/[id]/followings/index.dart index f7533ff..3843a6f 100644 --- a/backend/routes/api/users/[id]/followings/index.dart +++ b/backend/routes/api/users/[id]/followings/index.dart @@ -3,6 +3,7 @@ import 'package:stormberry/stormberry.dart'; import 'package:very_good_blog_app_backend/dtos/response/base_response_data.dart'; import 'package:very_good_blog_app_backend/dtos/response/users/followings/get_user_following_response.dart'; import 'package:very_good_blog_app_backend/models/following_follower.dart'; +import 'package:very_good_blog_app_backend/models/user.dart'; /// @Allow(GET) Future onRequest(RequestContext context, String id) { @@ -16,16 +17,27 @@ Future _onFollowingsByIdGetRequest( RequestContext context, String id, ) { - return context - .read() - .followingFollowers + final database = context.read(); + + return database.followingFollowers .queryFollowingFollowers( QueryParams( where: 'following_id = @id', values: {'id': id}, ), ) + .then( + (followingFollowerViews) async { + final followings = []; + for (final view in followingFollowerViews) { + final following = await database.users.queryUser(view.followingId); + if (following == null) continue; + followings.add(following); + } + return followings; + }, + ) .then((result) => result.map(GetUserFollowingResponse.fromView)) .then((res) => OkResponse(res.map((e) => e.toJson()).toList())) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } diff --git a/backend/routes/api/users/[id]/profiles/index.dart b/backend/routes/api/users/[id]/profiles/index.dart index 9782082..f147f1d 100644 --- a/backend/routes/api/users/[id]/profiles/index.dart +++ b/backend/routes/api/users/[id]/profiles/index.dart @@ -45,7 +45,7 @@ Future _onUserByIdGetRequest( ).toJson(), ), ) - .catchError((_) => ServerErrorResponse()); + .catchError((_) => InternalServerErrorResponse()); } Future _onUserByIdPatchRequest( @@ -73,7 +73,7 @@ Future _onUserByIdPatchRequest( ), ) .then((_) => OkResponse()) - .onError((e, _) => ServerErrorResponse(e.toString())); + .onError((e, _) => InternalServerErrorResponse(e.toString())); } on CheckedFromJsonException catch (e) { return BadRequestResponse(e.message); }