diff --git a/api/users/search.ts b/api/users/search.ts new file mode 100644 index 00000000..d31db3ec --- /dev/null +++ b/api/users/search.ts @@ -0,0 +1,153 @@ +import { VercelRequest, VercelResponse } from "@vercel/node"; +import { gql, request } from "graphql-request"; +import { Parser } from "json2csv"; +import { PROFILE_SUBGRAPH } from "../../utils"; + +type User = { + id: string; + internalId: string; + team: { id: string }; + isActive: boolean; + nftAddress: string; + tokenId: string; + totalPoints: string; +}; + +const getUsersFirstPage = async ( + pageSize: number, + team: string, + totalPointGreater: string, + totalPointLess: string, + nftAddress: string, + lastUserId: string +) => { + const teamQuery = (team && " team: $team ") || ""; + const totalPointsGTQuery = (totalPointGreater && " totalPoints_gt: $totalPointGreater ") || ""; + const totalPointsLTQuery = (totalPointLess && " totalPoints_lt: $totalPointLess ") || ""; + const nftAddressQuery = (nftAddress && " nftAddress: $nftAddress ") || ""; + const lastUserIdQuery = (lastUserId && " internalId_gt: $lastUserId ") || ""; + + const { users } = await request( + PROFILE_SUBGRAPH, + gql` + query getUsersQuery($pageSize: Int, $team: String, $totalPointGreater: String, $totalPointLess: String, $nftAddress: String, $lastUserId: String) { + users(orderBy: internalId, orderDirection: asc, first: $pageSize, where: ${ + "{" + + teamQuery + + totalPointsGTQuery + + totalPointsLTQuery + + nftAddressQuery + + lastUserIdQuery + + "}" + }) { + id + internalId + team { + id + } + isActive + nftAddress + tokenId + totalPoints + } + } + `, + { pageSize, team, totalPointGreater, totalPointLess, nftAddress, lastUserId } + ); + + return users; +}; + +const getCsv = (users: User[]) => { + const fields = [ + "id", + "internalId", + "team.id", + "isActive", + "nftAddress", + "tokenId", + "totalPoints", + ]; + + const parser = new Parser({ + fields, + }); + + return parser.parse(users); +}; + +export default async (req: VercelRequest, res: VercelResponse): Promise => { + if (req.method === "GET") { + try { + const { authorization } = req.headers; + if (authorization === `${process.env.PROFILE_API_SECRET_KEY}`) { + try { + const { + csv, + team, + totalPointGreater, + totalPointLess, + nftAddress, + page = "1", + } = req.query; + let { pageSize = "1000" } = req.query; + + const isCsv = csv === "true"; + if (parseInt(pageSize as string) > 1000 || isCsv) { + pageSize = "1000"; + } + + let lastUserId = ""; + let users: User[] = []; + + let fetchedPage = 0; + let allFetched = false; + while (fetchedPage < parseInt(page as string) && !allFetched) { + { + const fetchedUsers = await getUsersFirstPage( + parseInt(pageSize as string), + team as string, + totalPointGreater as string, + totalPointLess as string, + nftAddress as string, + lastUserId + ); + lastUserId = fetchedUsers[fetchedUsers.length - 1]?.internalId; + + if (isCsv) { + users = users.concat(fetchedUsers); + if (fetchedUsers.length < pageSize || lastUserId === undefined) { + allFetched = true; + } + } else { + users = fetchedUsers; + fetchedPage = fetchedPage + 1; + } + } + } + + if (isCsv) { + res.setHeader( + "Content-disposition", + `attachment; filename=profile-${new Date().toISOString()}.csv` + ); + res.setHeader("Content-Type", "text/csv"); + res.status(200).send(getCsv(users)); + } else { + res.status(200).json({ data: users, success: true }); + } + } catch (error) { + console.log(error); + throw new Error("Error refreshing Trading Competition Leaderboard"); + } + } else { + res.status(401).json({ success: false }); + } + } catch (err) { + res.status(500).json({ statusCode: 500 }); + } + } else { + res.setHeader("Allow", "GET"); + res.status(405).end("Method Not Allowed"); + } +}; diff --git a/package.json b/package.json index 8897fb60..fe6ba186 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,11 @@ } }, "dependencies": { + "@types/json2csv": "^5.0.3", "ethers": "^5.4.0", "graphql": "^15.5.0", "graphql-request": "^3.5.0", + "json2csv": "^5.0.6", "mongoose": "^5.13.0" }, "devDependencies": { diff --git a/utils/index.ts b/utils/index.ts index 2dc841aa..5f7fd18e 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -8,6 +8,8 @@ export const TRADING_COMPETITION_V2_SUBGRAPH = export const TRADING_COMPETITION_SUBGRAPH = TRADING_COMPETITION_V2_SUBGRAPH; +export const PROFILE_SUBGRAPH = "https://api.thegraph.com/subgraphs/name/pancakeswap/profile"; + /** * Check for the validity of a username based on rules (see documentation). * diff --git a/yarn.lock b/yarn.lock index cbe9fd2b..afea8744 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1142,6 +1142,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== +"@types/json2csv@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/json2csv/-/json2csv-5.0.3.tgz#759514772a90e35b08c10808dedeaf52248af418" + integrity sha512-ZJEv6SzhPhgpBpxZU4n/TZekbZqI4EcyXXRwms1lAITG2kIAtj85PfNYafUOY1zy8bWs5ujaub0GU4copaA0sw== + dependencies: + "@types/node" "*" + "@types/minimist@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" @@ -1731,6 +1738,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -3263,6 +3275,15 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +json2csv@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.6.tgz#590e0e1b9579e59baa53bda0c0d840f4d8009687" + integrity sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A== + dependencies: + commander "^6.1.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" + json5@2.x, json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -3279,7 +3300,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= @@ -3344,6 +3365,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"