diff --git a/src/api/controllers/nfts/controller.ts b/src/api/controllers/nfts/controller.ts index 436ae59..42e6abb 100644 --- a/src/api/controllers/nfts/controller.ts +++ b/src/api/controllers/nfts/controller.ts @@ -204,19 +204,6 @@ export class Controller { next(err) } } - - async getTopSellers( - req: Request, - res: Response, - next: NextFunction - ): Promise { - try { - const queryValues = validationGetFilters(req.query) - res.json(await NFTService.getTopSellers(queryValues)); - } catch (err) { - next(err) - } - } } export default new Controller(); diff --git a/src/api/controllers/nfts/router.ts b/src/api/controllers/nfts/router.ts index 5379d43..7d17c49 100644 --- a/src/api/controllers/nfts/router.ts +++ b/src/api/controllers/nfts/router.ts @@ -7,7 +7,6 @@ export default express .get("/most-viewed", controller.getMostViewed) .get("/most-sold", controller.getMostSold) .get("/most-sold-series", controller.getMostSoldSeries) - .get("/top-sellers", controller.getTopSellers) .get("/history", controller.getHistory) .get("/total-on-sale", controller.getTotalOnSale) .get("/:id", controller.getNFT) diff --git a/src/api/controllers/users/controller.ts b/src/api/controllers/users/controller.ts index 64c2dcd..6d64b0d 100644 --- a/src/api/controllers/users/controller.ts +++ b/src/api/controllers/users/controller.ts @@ -1,7 +1,7 @@ import UserService from "../../services/user"; import { NextFunction, Request, Response } from "express"; import { TERNOA_API_URL, decryptCookie } from "../../../utils"; -import { validationGetAccountBalance, validationGetUser, validationReviewRequested, validationGetUsers } from "../../validators/userValidators"; +import { validationGetAccountBalance, validationGetUser, validationReviewRequested, validationGetUsers, validationGetFilters } from "../../validators/userValidators"; export class Controller { async getUsers( @@ -99,5 +99,31 @@ export class Controller { next(err) } } + + async getTopSellers( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const queryValues = validationGetFilters(req.query) + res.json(await UserService.getTopSellers(queryValues)); + } catch (err) { + next(err) + } + } + + async getMostFollowed( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + const queryValues = validationGetFilters(req.query) + res.json(await UserService.getMostFollowed(queryValues)); + } catch (err) { + next(err) + } + } } export default new Controller(); diff --git a/src/api/controllers/users/router.ts b/src/api/controllers/users/router.ts index b0c3537..2cbad82 100644 --- a/src/api/controllers/users/router.ts +++ b/src/api/controllers/users/router.ts @@ -3,6 +3,8 @@ import controller from "./controller"; export default express .Router() .patch("/reviewRequested/:id", controller.reviewRequested) // ternoa-api + .get("/top-sellers", controller.getTopSellers) // ternoa-api + .get("/most-followed", controller.getMostFollowed) // ternoa-api .get("/", controller.getUsers) // ternoa-api .get("/verifyTwitter/:id", controller.verifyTwitter) // ternoa-api .get("/:id", controller.getUser) // ternoa-api diff --git a/src/api/helpers/nftHelpers.ts b/src/api/helpers/nftHelpers.ts index 55bcdc7..aa22b81 100644 --- a/src/api/helpers/nftHelpers.ts +++ b/src/api/helpers/nftHelpers.ts @@ -1,10 +1,10 @@ -import { CustomResponse, ICompleteNFT, INFT } from "../../interfaces/graphQL"; +import { ICompleteNFT, INFT } from "../../interfaces/graphQL"; import UserService from "../services/user"; import L from "../../common/logger"; import NFTService from "../services/nft"; import { ICategory } from "../../interfaces/ICategory"; import { fetchTimeout, isURL, removeURLSlash } from "../../utils"; -import { IUser } from "src/interfaces/IUser"; +import { IUser } from "../../interfaces/IUser"; import { NFTsQuery } from "../validators/nftValidators"; const ipfsGateways = { diff --git a/src/api/services/nft.ts b/src/api/services/nft.ts index 0873c3f..c8b6544 100644 --- a/src/api/services/nft.ts +++ b/src/api/services/nft.ts @@ -1,5 +1,4 @@ import { request } from "graphql-request"; -import fetch from "node-fetch"; import { DistinctNFTListResponse, INFT, NFTListResponse, CustomResponse, ISeries, INFTTransfer } from "../../interfaces/graphQL"; import FollowModel from "../../models/follow"; import NftModel from "../../models/nft"; @@ -8,12 +7,11 @@ import NftLikeModel from "../../models/nftLike"; import CategoryService from "./category" import { populateNFT } from "../helpers/nftHelpers"; import QueriesBuilder from "./gqlQueriesBuilder"; -import { decryptCookie, TERNOA_API_URL, TIME_BETWEEN_SAME_USER_VIEWS } from "../../utils"; +import { decryptCookie, TIME_BETWEEN_SAME_USER_VIEWS } from "../../utils"; import { canAddToSeriesQuery, addCategoriesNFTsQuery, getHistoryQuery, getSeriesStatusQuery, NFTBySeriesQuery, NFTQuery, NFTsQuery, statNFTsUserQuery, getTotalOnSaleQuery, likeUnlikeQuery, getFiltersQuery } from "../validators/nftValidators"; import CategoryModel from "../../models/category"; import { ICategory } from "../../interfaces/ICategory"; -import { INftLike } from "src/interfaces/INftLike"; -import { IUser } from "src/interfaces/IUser"; +import { INftLike } from "../../interfaces/INftLike"; const indexerUrl = process.env.INDEXER_URL || "https://indexer.chaos.ternoa.com"; @@ -514,37 +512,6 @@ export class NFTService { throw new Error("Couldn't get most sold series"); } } - - /** - * Get top sellers account address sorted by best sellers - * @param query - see getFiltersQuery - * @throws Will throw an error if indexer or db can't be reached - */ - async getTopSellers(query: getFiltersQuery): Promise> { - try { - const gqlQuery = QueriesBuilder.getTopSellers(query); - const res = await request(indexerUrl, gqlQuery); - const topSellers: {id: string, occurences: number}[] = res.topSeller.nodes; - const topSellersSorted = topSellers.map(x => x.id) - const filterDbUser = {walletIds: topSellersSorted} - const resDbUsers = await fetch(`${TERNOA_API_URL}/api/users/?filter=${JSON.stringify(filterDbUser)}`) - const dbUsers: CustomResponse = await resDbUsers.json() - const data = topSellersSorted.map(x => { - let user = dbUsers.data.find(y => y.walletId === x) - if (user === undefined) user = {_id: x, walletId: x} - return user - }) - const result: CustomResponse = { - totalCount: res.topSeller.totalCount, - data, - hasNextPage: res.topSeller.pageInfo.hasNextPage, - hasPreviousPage: res.topSeller.pageInfo.hasPreviousPage - } - return result - } catch (err) { - throw new Error("Couldn't get top sellers"); - } - } } export default new NFTService(); diff --git a/src/api/services/user.ts b/src/api/services/user.ts index e31c1fa..fd238f4 100644 --- a/src/api/services/user.ts +++ b/src/api/services/user.ts @@ -3,10 +3,12 @@ import fetch from "node-fetch"; import { IUser } from "../../interfaces/IUser"; import UserViewModel from "../../models/userView"; import NftLikeModel from "../../models/nftLike"; +import FollowModel from "../../models/follow"; import QueriesBuilder from "./gqlQueriesBuilder"; import { AccountResponse, Account, CustomResponse } from "../../interfaces/graphQL"; import { TIME_BETWEEN_SAME_USER_VIEWS, TERNOA_API_URL } from "../../utils"; import { getAccountBalanceQuery, getUserQuery, getUsersQuery } from "../validators/userValidators"; +import { getFiltersQuery } from "../validators/nftValidators"; const indexerUrl = process.env.INDEXER_URL || "https://indexer.chaos.ternoa.com"; @@ -93,6 +95,67 @@ export class UserService { } } + /** + * Get top sellers account address sorted by best sellers + * @param query - see getFiltersQuery + * @throws Will throw an error if indexer or db can't be reached + */ + async getTopSellers(query: getFiltersQuery): Promise> { + try { + const gqlQuery = QueriesBuilder.getTopSellers(query); + const res = await request(indexerUrl, gqlQuery); + const topSellers: {id: string, occurences: number}[] = res.topSeller.nodes; + const topSellersSorted = topSellers.map(x => x.id) + const filterDbUser = {walletIds: topSellersSorted} + const resDbUsers = await fetch(`${TERNOA_API_URL}/api/users/?filter=${JSON.stringify(filterDbUser)}`) + const dbUsers: CustomResponse = await resDbUsers.json() + const data = topSellersSorted.map(x => { + let user = dbUsers.data.find(y => y.walletId === x) + if (user === undefined) user = {_id: x, walletId: x} + return user + }) + const result: CustomResponse = { + totalCount: res.topSeller.totalCount, + data, + hasNextPage: res.topSeller.pageInfo.hasNextPage, + hasPreviousPage: res.topSeller.pageInfo.hasPreviousPage + } + return result + } catch (err) { + throw new Error("Couldn't get top sellers"); + } + } + + /** + * get most followed users sorted by number of follows + * @param query - see getFiltersQuery + * @throws Will throw an error if db can't be reached + */ + async getMostFollowed(query: getFiltersQuery): Promise> { + try{ + const aggregateQuery = [{ $group: { _id: "$followed", totalViews: { $sum: 1 } } }] + const aggregate = FollowModel.aggregate(aggregateQuery); + const res = await FollowModel.aggregatePaginate(aggregate, {page: query.pagination.page, limit: query.pagination.limit, sort:{totalViews: -1}}) + const walletIdsSorted = res.docs.map(x => x._id) + const filterDbUser = {walletIds: walletIdsSorted} + const resDbUsers = await fetch(`${TERNOA_API_URL}/api/users/?filter=${JSON.stringify(filterDbUser)}`) + const dbUsers: CustomResponse = await resDbUsers.json() + const data = walletIdsSorted.map(x => { + let user = dbUsers.data.find(y => y.walletId === x) + if (user === undefined) user = {_id: x, walletId: x} + return user + }) + const result: CustomResponse = { + totalCount: res.totalDocs, + data, + hasNextPage: res.hasNextPage, + hasPreviousPage: res.hasPrevPage + } + return result + }catch(err){ + throw err + } + } } export default new UserService(); diff --git a/src/api/validators/userValidators.ts b/src/api/validators/userValidators.ts index 527da8c..ce41df7 100644 --- a/src/api/validators/userValidators.ts +++ b/src/api/validators/userValidators.ts @@ -75,4 +75,23 @@ export const validationGetAccountBalance = (query: any) => { id: Joi.string().required(), }); return validateQuery(validationSchema, query) as getAccountBalanceQuery; +}; + + +export type getFiltersQuery = { + pagination: { + page: number; + limit: number; + }; +}; +export const validationGetFilters = (query: any) => { + let { pagination } = query; + if (pagination) pagination = JSON.parse(pagination); + const validationSchema = Joi.object({ + pagination: Joi.object({ + page: Joi.number().integer().min(0).required(), + limit: Joi.number().integer().min(0).max(LIMIT_MAX_PAGINATION).required(), + }).required(), + }); + return validateQuery(validationSchema, { pagination }) as getFiltersQuery; }; \ No newline at end of file diff --git a/src/models/follow.ts b/src/models/follow.ts index 723cdd4..17809a9 100644 --- a/src/models/follow.ts +++ b/src/models/follow.ts @@ -1,6 +1,6 @@ -import mongoose, { PaginateModel } from "mongoose"; +import mongoose, {AggregatePaginateModel} from "mongoose"; import { IFollow } from "../interfaces/IFollow"; -import mongoosePaginate from "mongoose-paginate-v2"; +import aggregatePaginate from "mongoose-aggregate-paginate-v2"; /* based on socialite implementation https://github.com/mongodb-labs/socialite/blob/master/docs/graph.md */ @@ -15,11 +15,11 @@ const Follow = new mongoose.Schema({ }, }); -Follow.plugin(mongoosePaginate); +Follow.plugin(aggregatePaginate); const FollowModel = mongoose.model( "Follow", Follow -) as PaginateModel; +) as unknown as AggregatePaginateModel; export default FollowModel;