diff --git a/package.json b/package.json index 045d74c..144f444 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nft-marketplace-api", - "version": "1.2.1", + "version": "1.3.0", "description": "ternoa nft marketplace server", "main": "dist/index.js", "scripts": { diff --git a/src/api/controllers/nfts/controller.ts b/src/api/controllers/nfts/controller.ts index 35df1dc..3ad819d 100644 --- a/src/api/controllers/nfts/controller.ts +++ b/src/api/controllers/nfts/controller.ts @@ -1,6 +1,6 @@ import NFTService from "../../services/nft"; import { NextFunction, Request, Response } from "express"; -import { validationGetNFTs, validationGetNFT, validationGetStatNFTsUser, validationNFTsBySeries, validationGetSeries, validationCanAddToSeries, validationGetHistory, validationAddCategoriesNFTs } from "../../validators/nftValidators"; +import { validationGetNFTs, validationGetNFT, validationGetStatNFTsUser, validationNFTsBySeries, validationGetSeries, validationCanAddToSeries, validationGetHistory, validationAddCategoriesNFTs, validationGetTotalOnSale } from "../../validators/nftValidators"; export class Controller { async getNFTs( @@ -96,12 +96,25 @@ export class Controller { next: NextFunction ): Promise{ try { - const queryValues = validationGetHistory({ ...req.query }) + const queryValues = validationGetHistory(req.query) res.json(await NFTService.getHistory(queryValues)); } catch (err) { next(err); } } + + async getTotalOnSale( + req: Request, + res: Response, + next: NextFunction + ): Promise{ + try { + const queryValues = validationGetTotalOnSale(req.query) + res.json(await NFTService.getTotalOnSale(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 dd77fee..4401c71 100644 --- a/src/api/controllers/nfts/router.ts +++ b/src/api/controllers/nfts/router.ts @@ -4,6 +4,7 @@ export default express .Router() .get("/", controller.getNFTs) .get("/history", controller.getHistory) + .get("/total-on-sale", controller.getTotalOnSale) .get("/:id", controller.getNFT) .get("/stat/:id", controller.getStatNFTsUser) .get("/series/data", controller.getNFTsBySeries) diff --git a/src/api/helpers/nftHelpers.ts b/src/api/helpers/nftHelpers.ts index c159c18..55bcdc7 100644 --- a/src/api/helpers/nftHelpers.ts +++ b/src/api/helpers/nftHelpers.ts @@ -24,83 +24,38 @@ const ipfsGatewayUri = */ export async function populateNFT( NFT: INFT, - seriesData: CustomResponse, query: NFTsQuery ): Promise { - const [serieData, creatorData, ownerData, info, categories, seriesLocked] = + const [stat, creatorData, ownerData, info, categories, seriesLocked] = await Promise.all([ - populateSerieData(NFT, seriesData, query), + populateStat(NFT, query), populateNFTCreator(NFT), populateNFTOwner(NFT), populateNFTIpfs(NFT), populateNFTCategories(NFT), populateNFTSeriesObject(NFT.serieId) ]); - return { ...NFT, ...serieData, creatorData, ownerData, ...info, categories, seriesLocked}; + return { ...NFT, ...stat, creatorData, ownerData, ...info, categories, seriesLocked}; } -export async function populateSerieData( +export async function populateStat( NFT: INFT, - seriesData: CustomResponse, query: NFTsQuery ): Promise<{ - serieData: INFT[]; - totalNft: number; - totalListedNft: number; - totalListedInMarketplace: number; - totalOwnedByRequestingUser: number; - totalOwnedListedByRequestingUser: number; - smallestPrice: string; + totalNft: number, + totalListedNft: number, + totalListedInMarketplace: number, + totalOwnedByRequestingUser: number, + totalOwnedListedByRequestingUser: number, + smallestPrice: string }> { try { const marketplaceId = query.filter?.marketplaceId; const owner = query.filter?.owner; - if (NFT.serieId === "0") - return { - serieData: [ - { - id: NFT.id, - owner: NFT.owner, - listed: NFT.listed, - price: NFT.price, - marketplaceId: NFT.marketplaceId, - }, - ], - totalNft: 1, - totalListedNft: NFT.listed, - totalListedInMarketplace: NFT.listed, - totalOwnedByRequestingUser: 1, - totalOwnedListedByRequestingUser: NFT.listed, - smallestPrice: NFT.price, - }; - const result = seriesData.data.filter((x) => x.serieId === NFT.serieId).map(({serieId, ...rest}) => rest); - const serieData = result.sort( - (a, b) => - (a.isCapsule !== b.isCapsule && (a.isCapsule ? 1 : -1)) || // capsule last - b.listed - a.listed || // listed first - (marketplaceId && Number(a.marketplaceId) !== Number(b.marketplaceId) && (Number(a.marketplaceId) === marketplaceId || Number(b.marketplaceId) === marketplaceId) && - marketplaceId === Number(a.marketplaceId) ? -1 : 1) || // marketplace id corresponding to request first - Number(a.price) - Number(b.price) // smallest price first - ); - const listedNft = serieData.filter((x) => x.listed); - return { - serieData: !query.filter?.noSeriesData ? serieData : [], - totalNft: serieData.length, - totalListedNft: listedNft.length, - totalListedInMarketplace: marketplaceId - ? listedNft.filter((x) => Number(x.marketplaceId) === marketplaceId) - .length - : listedNft.length, - totalOwnedByRequestingUser: owner - ? serieData.filter((x) => x.owner === owner).length - : 0, - totalOwnedListedByRequestingUser: owner - ? listedNft.filter((x) => x.owner === owner).length - : 0, - smallestPrice: serieData.length > 0 ? serieData[0].price : NFT.price - }; + const stat = await NFTService.getStatNFT(NFT.serieId, marketplaceId, owner) + return stat } catch (err) { - L.error({ err }, "NFTs with same serie could not have been fetched"); + L.error({ err }, "NFTs stats could not have been fetched"); return null; } } diff --git a/src/api/services/gqlQueriesBuilder.ts b/src/api/services/gqlQueriesBuilder.ts index 1057eae..a270a88 100644 --- a/src/api/services/gqlQueriesBuilder.ts +++ b/src/api/services/gqlQueriesBuilder.ts @@ -1,6 +1,7 @@ import { gql } from "graphql-request"; import { convertSortString, LIMIT_MAX_PAGINATION } from "../../utils"; import { getHistoryQuery, getSeriesStatusQuery, NFTBySeriesQuery, NFTQuery, NFTsQuery, statNFTsUserQuery } from "../validators/nftValidators"; +// import L from '../../common/logger'; const nodes = ` nodes { @@ -29,7 +30,6 @@ export class GQLQueriesBuilder { offset: ${query.pagination?.page && query.pagination?.limit ? (query.pagination.page - 1) * query.pagination.limit : 0} filter:{ and:[ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } ${query.filter?.ids ? `{id: { in: ${JSON.stringify(query.filter.ids.map(x => String(x)))} }}` : ""} ${query.filter?.idsToExclude ? `{id: { notIn: ${JSON.stringify(query.filter.idsToExclude.map(x => String(x)))} }}` : ""} ${query.filter?.idsCategories ? `{id: { in: ${JSON.stringify(query.filter.idsCategories.map(x => String(x)))} }}` : ""} @@ -73,7 +73,6 @@ export class GQLQueriesBuilder { nftEntities( filter: { and: [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } { id: { equalTo: "${query.id}" } } ] @@ -92,7 +91,6 @@ export class GQLQueriesBuilder { listed price marketplaceId - serieId isCapsule } `; @@ -105,11 +103,12 @@ export class GQLQueriesBuilder { ` : ""} filter: { and : [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } { serieId:{ in:${JSON.stringify(query.seriesIds)} } } + ${query.filter?.owner ? `{ owner: { equalTo: "${query.filter.owner}" } }` : ""} ] } + orderBy: [IS_CAPSULE_ASC, LISTED_DESC] ) { totalCount @@ -128,7 +127,6 @@ export class GQLQueriesBuilder { nftEntities( filter: { and: [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } { owner: { equalTo: "${query.id}" } } ] @@ -143,7 +141,6 @@ export class GQLQueriesBuilder { nftEntities( filter: { and: [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } ${query.filter?.marketplaceId ? `{ marketplaceId: { equalTo: "${query.filter.marketplaceId}"} }` : ""} { owner: { equalTo: "${query.id}" } } @@ -161,7 +158,6 @@ export class GQLQueriesBuilder { nftEntities( filter: { and: [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } { owner: { equalTo: "${query.id}" } } {listed: { equalTo: 0}} @@ -178,7 +174,6 @@ export class GQLQueriesBuilder { nftEntities( filter: { and: [ - { serieId: { notEqualTo: "Ternoa Xmas 2021" } } { timestampBurn: { isNull: true } } { creator: { equalTo: "${query.id}" } } ] @@ -276,6 +271,140 @@ export class GQLQueriesBuilder { } `; +countTotal = (seriesId: string) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + ] + } + ) { + totalCount + } + } +`; + +countTotalListed = (seriesId: string) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { listed: { equalTo: 1} } + ] + } + ) { + totalCount + } + } +`; + +countTotalListedInMarketplace = (seriesId: string, marketplaceId: number) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { listed: { equalTo: 1} } + { marketplaceId: { equalTo: "${marketplaceId}" } } + ] + } + ) { + totalCount + } + } +`; + +countTotalOwned = (seriesId: string, owner: string) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { owner: { equalTo: "${owner}" } } + ] + } + ) { + totalCount + } + } +`; + +countTotalOwnedListed = (seriesId: string, owner: string) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { listed: { equalTo: 1} } + { owner: { equalTo: "${owner}" } } + ] + } + ) { + totalCount + } + } +`; + +countTotalOwnedListedInMarketplace = (seriesId: string, owner: string, marketplaceId: number) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { listed: { equalTo: 1} } + { owner: { equalTo: "${owner}" } } + { marketplaceId: { equalTo: "${marketplaceId}" } } + ] + } + ) { + totalCount + } + } +`; + +countSmallestPrice = (seriesId: string, marketplaceId: number=null) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { serieId: { equalTo: "${seriesId}" } } + { listed: { equalTo: 1} } + ${marketplaceId ? `{ marketplaceId: {equalTo: ${marketplaceId}} }` : ``} + ] + } + ) { + nodes{ + price + } + } + } +`; + +countAllListedInMarketplace = (marketplaceId: number) => gql` + { + nftEntities( + filter: { + and: [ + { timestampBurn: { isNull: true } } + { listed: { equalTo: 1} } + { marketplaceId: { equalTo: "${marketplaceId}" } } + ] + } + ) { + totalCount + } + } +`; + } export default new GQLQueriesBuilder(); diff --git a/src/api/services/nft.ts b/src/api/services/nft.ts index ab7b6fd..6be76a2 100644 --- a/src/api/services/nft.ts +++ b/src/api/services/nft.ts @@ -9,11 +9,12 @@ 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 { canAddToSeriesQuery, addCategoriesNFTsQuery, getHistoryQuery, getSeriesStatusQuery, NFTBySeriesQuery, NFTQuery, NFTsQuery, statNFTsUserQuery } from "../validators/nftValidators"; +import { canAddToSeriesQuery, addCategoriesNFTsQuery, getHistoryQuery, getSeriesStatusQuery, NFTBySeriesQuery, NFTQuery, NFTsQuery, statNFTsUserQuery, getTotalOnSaleQuery } from "../validators/nftValidators"; import { IUser } from "../../interfaces/IUser"; import CategoryModel from "../../models/category"; import { ICategory } from "../../interfaces/ICategory"; // import { INFTLike } from "../../interfaces/INFTLike"; +import L from "../../common/logger"; const indexerUrl = process.env.INDEXER_URL || "https://indexer.chaos.ternoa.com"; @@ -25,34 +26,34 @@ export class NFTService { */ async getNFTs(query: NFTsQuery): Promise> { try { - // const likesData: AggregatePaginateResult<{_id: string, count: number}> | null = null // Categories - if (query.filter?.categories){ + if (query.filter?.categories) { const withNoCategories = query.filter.categories.includes("none") - const categoriesCode = query.filter.categories.filter(x => x!=="none") + const categoriesCode = query.filter.categories.filter(x => x !== "none") const allCategories = await CategoryService.getCategories({}) const categories = allCategories.map(x => x.code).filter(x => categoriesCode.includes(x)) - const mongoQuery = {categories: {$in: categories} } + const mongoQuery = { categories: { $in: categories } } const mongoNfts = await NftModel.find(mongoQuery as any) const nftIds = mongoNfts.map((nft) => nft.chainId) - if (withNoCategories){ + if (withNoCategories) { const categoriesToExclude = allCategories.map(x => x.code).filter(x => !categoriesCode.includes(x)) - const mongoQueryExclude = {$and: [{categories: {$in: categoriesToExclude}}, {chainId: {$nin: nftIds}}]} + const mongoQueryExclude = { $and: [{ categories: { $in: categoriesToExclude } }, { chainId: { $nin: nftIds } }] } const mongoNFTsToExclude = await NftModel.find(mongoQueryExclude as any) const nftIdsToExclude = mongoNFTsToExclude.map((nft) => nft.chainId) query.filter.idsToExcludeCategories = nftIdsToExclude - }else{ + } else { query.filter.idsCategories = nftIds } } // Liked only - if (query.filter?.liked){ + if (query.filter?.liked) { const data = await fetch(`${TERNOA_API_URL}/api/users/${query.filter.liked}?populateLikes=${true}`) const user = await data.json() as IUser - query.filter.series = user.likedNFTs.length > 0 ? user.likedNFTs.map(x=>x.serieId) : [] + query.filter.series = user.likedNFTs.length > 0 ? user.likedNFTs.map(x => x.serieId) : [] } - // Sort mongo + // Sort mongo -> BLOCKED CAUSE OF DATASTRUCTURE + // const likesData: AggregatePaginateResult<{_id: string, count: number}> | null = null /*if (query.sortMongo){ const sortArray = query.sortMongo.split(',') const sortByLikes = sortArray.find(x => x.split(':')[0] === "likes") @@ -74,12 +75,10 @@ export class NFTService { const gqlQuery = QueriesBuilder.NFTs(query); const res: DistinctNFTListResponse = await request(indexerUrl, gqlQuery); const NFTs = res.distinctSerieNfts.nodes; - // Series Data - const seriesData = await this.getNFTsForSeries({seriesIds: NFTs.map(x => x.serieId)}) // Populate - res.distinctSerieNfts.nodes = await Promise.all(NFTs.map(async (NFT) => populateNFT(NFT, seriesData, query))) + res.distinctSerieNfts.nodes = await Promise.all(NFTs.map(async (NFT) => populateNFT(NFT, query))) // Result formatting - const result: CustomResponse={ + const result: CustomResponse = { totalCount: res.distinctSerieNfts.totalCount, data: res.distinctSerieNfts.nodes, hasNextPage: res.distinctSerieNfts.pageInfo?.hasNextPage || undefined, @@ -104,26 +103,25 @@ export class NFTService { const result: NFTListResponse = await request(indexerUrl, gqlQuery); let NFT = result.nftEntities.nodes[0]; if (!NFT) throw new Error(); - const seriesData = await this.getNFTsForSeries({seriesIds: [NFT.serieId]}) - NFT = await populateNFT(NFT, seriesData, query); + NFT = await populateNFT(NFT, query); let viewsCount = 0 - if (query.incViews){ + if (query.incViews) { const date = +new Date() - const views = await NftViewModel.find(NFT.serieId!=="0" ? {viewedSerie: NFT.serieId} : {viewedId: query.id}) - if (query.viewerIp && - ( - views.length === 0 || - date - Math.max.apply(null, views.filter(x => x.viewerIp === query.viewerIp).map(x => x.date)) > TIME_BETWEEN_SAME_USER_VIEWS - ) + const views = await NftViewModel.find(NFT.serieId !== "0" ? { viewedSerie: NFT.serieId } : { viewedId: query.id }) + if (query.viewerIp && + ( + views.length === 0 || + date - Math.max.apply(null, views.filter(x => x.viewerIp === query.viewerIp).map(x => x.date)) > TIME_BETWEEN_SAME_USER_VIEWS + ) ) { - const newView = new NftViewModel({viewedSerie: NFT.serieId, viewedId: query.id, viewer: query.viewerWalletId, viewerIp: query.viewerIp, date}) + const newView = new NftViewModel({ viewedSerie: NFT.serieId, viewedId: query.id, viewer: query.viewerWalletId, viewerIp: query.viewerIp, date }) await newView.save(); viewsCount = views.length + 1 - }else{ + } else { viewsCount = views.length } } - return { ...NFT, viewsCount}; + return { ...NFT, viewsCount }; } catch (err) { throw new Error("Couldn't get NFT"); } @@ -134,14 +132,14 @@ export class NFTService { * @param query - query (see statNFTsUserQuery) * @throws Will throw an error if can't request indexer or db or user not find */ - async getStatNFTsUser(query: statNFTsUserQuery): Promise<{ - countOwned: number, - countOwnedListed: number, - countOwnedUnlisted: number, - countCreated: number, - countFollowers: number, + async getStatNFTsUser(query: statNFTsUserQuery): Promise<{ + countOwned: number, + countOwnedListed: number, + countOwnedUnlisted: number, + countCreated: number, + countFollowers: number, countFollowed: number - }> { + }> { try { const [owned, ownedListed, ownedUnlisted, created, followers, followed] = await Promise.all([ request(indexerUrl, QueriesBuilder.countOwnerOwned(query)), @@ -157,12 +155,55 @@ export class NFTService { const countCreated: number = created.nftEntities.totalCount; const countFollowers: number = followers.length const countFollowed: number = followed.length - return {countOwned, countOwnedListed, countOwnedUnlisted, countCreated, countFollowers, countFollowed} + return { countOwned, countOwnedListed, countOwnedUnlisted, countCreated, countFollowers, countFollowed } } catch (err) { throw new Error("Couldn't get users stat"); } } + /** + * Gets nft stat (total, total listed, listed, total listed on marketplace, owned by user, owned by user listed, smallest price) + * @param seriesId - Series Id of nft to get stat for + * @param marketplaceId - marketplace id (optional) + * @param owner - owner (optional) + * @throws Will throw an error if can't request indexer + */ + async getStatNFT(seriesId:string, marketplaceId:number=null, owner:string=null): Promise<{ + totalNft: number, + totalListedNft: number, + totalListedInMarketplace: number, + totalOwnedByRequestingUser: number, + totalOwnedListedByRequestingUser: number, + totalOwnedListedInMarketplaceByRequestingUser: number, + smallestPrice: string + }> { + try { + const [totalRequest, totalListedRequest, totalListedInMarketplaceRequest, totalOwnedByRequestingUserRequest, totalOwnedListedByRequestingUserRequest, totalOwnedListedInMarketplaceByRequestingUserRequest, smallestPriceRequest] = await Promise.all([ + request(indexerUrl, QueriesBuilder.countTotal(seriesId)), + request(indexerUrl, QueriesBuilder.countTotalListed(seriesId)), + marketplaceId!==null ? request(indexerUrl, QueriesBuilder.countTotalListedInMarketplace(seriesId, marketplaceId)) : 0, + owner ? request(indexerUrl, QueriesBuilder.countTotalOwned(seriesId, owner)) : null, + owner ? request(indexerUrl, QueriesBuilder.countTotalOwnedListed(seriesId, owner)) : null, + owner && marketplaceId!==null ? request(indexerUrl, QueriesBuilder.countTotalOwnedListedInMarketplace(seriesId, owner, marketplaceId)) : null, + request(indexerUrl, QueriesBuilder.countSmallestPrice(seriesId, marketplaceId)), + ]) + const totalNft: number = totalRequest.nftEntities.totalCount; + const totalListedNft: number = totalListedRequest.nftEntities.totalCount; + const totalListedInMarketplace: number = totalListedInMarketplaceRequest ? totalListedInMarketplaceRequest.nftEntities.totalCount : 0; + const totalOwnedByRequestingUser: number = totalOwnedByRequestingUserRequest ? totalOwnedByRequestingUserRequest.nftEntities.totalCount : 0; + const totalOwnedListedByRequestingUser: number = totalOwnedListedByRequestingUserRequest ? totalOwnedListedByRequestingUserRequest.nftEntities.totalCount : 0; + const totalOwnedListedInMarketplaceByRequestingUser: number = totalOwnedListedInMarketplaceByRequestingUserRequest ? totalOwnedListedInMarketplaceByRequestingUserRequest.nftEntities.totalCount : 0; + const smallestPrice: string = smallestPriceRequest.nftEntities.nodes.length > 0 ? + smallestPriceRequest.nftEntities.nodes.sort((a:INFT,b:INFT) => Number(a.price) - Number(b.price))[0].price + : + "0" + ; + return { totalNft, totalListedNft, totalListedInMarketplace, totalOwnedByRequestingUser, totalOwnedListedByRequestingUser, totalOwnedListedInMarketplaceByRequestingUser, smallestPrice } + } catch (err) { + throw new Error("Couldn't get NFT stat"); + } + } + /** * Creates a new nft document in DB (for offchain categories) * @param query - query (see addCategoriesNFTsQuery) @@ -176,13 +217,13 @@ export class NFTService { decryptedCreator === query.creator && decryptedChainIdsString === query.chainIds.join('-') && decryptedCategoriesString === query.categories.join('-') - ){ - const categories = await CategoryService.getCategories({filter: {codes: query.categories}}) + ) { + const categories = await CategoryService.getCategories({ filter: { codes: query.categories } }) const categoriesCodes = categories.map(x => x.code) - const data: {chainId: string, categories: string[]}[] = query.chainIds.map(x => { return {chainId: x, categories: categoriesCodes} }) + const data: { chainId: string, categories: string[] }[] = query.chainIds.map(x => { return { chainId: x, categories: categoriesCodes } }) await NftModel.insertMany(data) return true; - }else{ + } else { throw new Error("Invalid authentication") } } catch (err) { @@ -195,18 +236,24 @@ export class NFTService { * @param query - query (see NFTBySeriesQuery) * @throws Will throw an error if nft ID doesn't exist */ - async getNFTsForSeries(query: NFTBySeriesQuery): Promise>{ - try{ + async getNFTsForSeries(query: NFTBySeriesQuery): Promise> { + try { + const marketplaceId = query.filter?.marketplaceId; const gqlQuery = QueriesBuilder.NFTsForSeries(query) const res: NFTListResponse = await request(indexerUrl, gqlQuery); - const result: CustomResponse={ + const seriesData = res.nftEntities.nodes.sort((a, b) => + (marketplaceId && Number(a.marketplaceId) !== Number(b.marketplaceId) && (Number(a.marketplaceId) === marketplaceId || Number(b.marketplaceId) === marketplaceId) && + marketplaceId === Number(a.marketplaceId) ? -1 : 1) || // marketplace id corresponding to request first + Number(a.price) - Number(b.price) // smallest price first + ) + const result: CustomResponse = { totalCount: res.nftEntities.totalCount, - data: res.nftEntities.nodes, + data: seriesData, hasNextPage: res.nftEntities.pageInfo?.hasNextPage || undefined, hasPreviousPage: res.nftEntities.pageInfo?.hasPreviousPage || undefined } return result - }catch(err){ + } catch (err) { throw new Error("Couldn't get NFTs for those series"); } } @@ -220,7 +267,7 @@ export class NFTService { try { const nft = await NftModel.findOne({ chainId: nftId }); if (!nft) return null; - const categories = await CategoryModel.find({code: {$in: nft.categories}}) + const categories = await CategoryModel.find({ code: { $in: nft.categories } }) return categories as ICategory[]; } catch (err) { throw new Error("Couldn't get categories for this NFT"); @@ -232,13 +279,13 @@ export class NFTService { * @param query - query (see getSeriesStatusQuery) * @throws Will throw an error if seriesId is not found */ - async getSeriesStatus(query: getSeriesStatusQuery): Promise{ - try{ + async getSeriesStatus(query: getSeriesStatusQuery): Promise { + try { const gqlQuery = QueriesBuilder.getSeries(query) const res = await request(indexerUrl, gqlQuery); if (!res.serieEntities.nodes || res.serieEntities.nodes.length === 0) throw Error() return res.serieEntities.nodes[0] - }catch(err){ + } catch (err) { throw new Error("Couldn't get series status"); } } @@ -248,44 +295,44 @@ export class NFTService { * @param query - query (see canAddToSeriesQuery) * @throws Will throw an error if seriesId is not found */ - async canAddToSeries(query: canAddToSeriesQuery): Promise{ - try{ - const gqlQuery = QueriesBuilder.getSeries(query) - const res = await request(indexerUrl, gqlQuery); - if (!res.serieEntities.nodes || res.serieEntities.nodes.length === 0) return true - const series:ISeries = res.serieEntities.nodes[0] - if (series.locked || series.owner!==query.walletId) return false - return true - }catch(err){ - throw new Error("Couldn't get information about this series"); - } + async canAddToSeries(query: canAddToSeriesQuery): Promise { + try { + const gqlQuery = QueriesBuilder.getSeries(query) + const res = await request(indexerUrl, gqlQuery); + if (!res.serieEntities.nodes || res.serieEntities.nodes.length === 0) return true + const series: ISeries = res.serieEntities.nodes[0] + if (series.locked || series.owner !== query.walletId) return false + return true + } catch (err) { + throw new Error("Couldn't get information about this series"); } + } /** * Return the history of the serie specified * @param query - query (see getHistoryQuery) * @throws Will throw an error if indexer is not reachable */ - async getHistory(query: getHistoryQuery): Promise>{ - try{ + async getHistory(query: getHistoryQuery): Promise> { + try { const gqlQuery = QueriesBuilder.getHistory(query) const res = await request(indexerUrl, gqlQuery); const data: INFTTransfer[] = [] - if (query.filter?.grouped){ - let previousRow:INFTTransfer = null + if (query.filter?.grouped) { + let previousRow: INFTTransfer = null let tempQty = 1 res.nftTransferEntities.nodes.forEach((x: INFTTransfer) => { const currentRow = x - if (previousRow){ + if (previousRow) { if ( currentRow.from === previousRow.from && - currentRow.to === previousRow.to && + currentRow.to === previousRow.to && currentRow.amount === previousRow.amount && currentRow.seriesId === previousRow.seriesId && currentRow.typeOfTransaction === previousRow.typeOfTransaction - ){ + ) { tempQty += 1 - }else{ + } else { previousRow.quantity = tempQty data.push(previousRow) tempQty = 1 @@ -293,22 +340,38 @@ export class NFTService { } previousRow = currentRow }); - if (previousRow){ + if (previousRow) { previousRow.quantity = tempQty data.push(previousRow) } } - const result: CustomResponse={ + const result: CustomResponse = { totalCount: res.nftTransferEntities.totalCount, data: query.filter?.grouped ? data : res.nftTransferEntities.nodes, hasNextPage: res.nftTransferEntities.pageInfo?.hasNextPage || undefined, hasPreviousPage: res.nftTransferEntities.pageInfo?.hasPreviousPage || undefined } return result - }catch(err){ + } catch (err) { throw new Error("Couldn't get history information about this nft / series"); } } + + /** + * Returns the + * @param query - query (see getTotalOnSaleQuery) + * @throws Will throw an error if seriesId is not found + */ + async getTotalOnSale(query: getTotalOnSaleQuery): Promise { + try { + const gqlQuery = QueriesBuilder.countAllListedInMarketplace(query.marketplaceId) + const res = await request(indexerUrl, gqlQuery); + if (!res.nftEntities.totalCount) throw new Error() + return res.nftEntities.totalCount + } catch (err) { + throw new Error("Count could not have been fetched"); + } + } } export default new NFTService(); diff --git a/src/api/validators/nftValidators.ts b/src/api/validators/nftValidators.ts index c87d4d0..df22fc4 100644 --- a/src/api/validators/nftValidators.ts +++ b/src/api/validators/nftValidators.ts @@ -27,7 +27,6 @@ export type NFTsQuery = { priceTiimeFilter?: string seriesLocked?: boolean isCapsule?: boolean - noSeriesData?: boolean } } export const validationGetNFTs = (query: any) => { @@ -58,7 +57,6 @@ export const validationGetNFTs = (query: any) => { priceTiimeFilter: Joi.string(), seriesLocked: Joi.boolean(), isCapsule: Joi.boolean(), - noSeriesData: Joi.boolean(), }), }) return validateQuery(validationSchema, { pagination, sort, sortMongo, filter }) as NFTsQuery; @@ -72,7 +70,7 @@ export type NFTQuery = { incViews?: boolean filter?: { marketplaceId?: number - noSeriesData?: boolean + owner?:string, } } export const validationGetNFT = (query: any) => { @@ -83,7 +81,7 @@ export const validationGetNFT = (query: any) => { id: Joi.number().required().integer().min(0), filter: Joi.object({ marketplaceId: Joi.number().integer().min(0), - noSeriesData: Joi.boolean(), + owner:Joi.string(), }), incViews: Joi.boolean(), viewerWalletId: Joi.string(), @@ -119,19 +117,29 @@ export type NFTBySeriesQuery = { page?: number limit?: number } + filter?:{ + owner?: string + marketplaceId?: number + } } + export const validationNFTsBySeries = (query: any) => { - let { pagination, seriesIds } = query + let { pagination, seriesIds, filter } = query seriesIds = typeof seriesIds === "string" ? [seriesIds] : seriesIds if (pagination) pagination = JSON.parse(pagination); + if (filter) filter = JSON.parse(filter); const validationSchema = Joi.object({ seriesIds: Joi.array().required().items(Joi.string().required()), pagination: Joi.object({ page: Joi.number().integer().min(0), limit: Joi.number().integer().min(0).max(LIMIT_MAX_PAGINATION), - }) + }), + filter: Joi.object({ + owner: Joi.string(), + marketplaceId: Joi.number().integer(), + }), }) - return validateQuery(validationSchema, { pagination, seriesIds }) as NFTBySeriesQuery; + return validateQuery(validationSchema, { pagination, seriesIds, filter }) as NFTBySeriesQuery; } @@ -219,4 +227,14 @@ export const validationGetHistory = (query: any) => { }).required(), }) return validateQuery(validationSchema, {pagination, sort, filter}) as getHistoryQuery; -} \ No newline at end of file +} + +export type getTotalOnSaleQuery = { + marketplaceId: number +} +export const validationGetTotalOnSale = (query: any) => { + const validationSchema = Joi.object({ + marketplaceId: Joi.number().required(), + }) + return validateQuery(validationSchema, query) as getTotalOnSaleQuery; +}