Skip to content

Commit

Permalink
feat: Add get all places endpoint (#560)
Browse files Browse the repository at this point in the history
* feat: Add task to fetch worlds live users

* feat: Add endpoint to return merged Places and Worlds scene info

* feat: Add tests

* fix: Use max limit constant

* feat: Add utils tests

* feat: Add env var WORLDS_LIVE_DATA
  • Loading branch information
cyaiox authored Nov 19, 2024
1 parent 93125fd commit 0533ea4
Show file tree
Hide file tree
Showing 15 changed files with 1,211 additions and 55 deletions.
450 changes: 450 additions & 0 deletions src/__data__/allPlacesWithAggregatedAttributes.ts

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/__data__/worldsLiveData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { WorldLiveDataProps } from "../entities/World/types"

export const worldsLiveData: WorldLiveDataProps = {
perWorld: [{ worldName: "test.dcl.eth", users: 30 }],
totalUsers: 30,
}
Original file line number Diff line number Diff line change
@@ -1,162 +1,179 @@
import { Request } from "decentraland-gatsby/dist/entities/Route/wkc/request/Request"

import { allPlacesWithAggregatedAttributes } from "../../../__data__/allPlacesWithAggregatedAttributes"
import { hotSceneGenesisPlaza } from "../../../__data__/hotSceneGenesisPlaza"
import { placeGenesisPlazaWithAggregatedAttributes } from "../../../__data__/placeGenesisPlazaWithAggregatedAttributes"
import { sceneStatsGenesisPlaza } from "../../../__data__/sceneStatsGenesisPlaza"
import { worldsLiveData } from "../../../__data__/worldsLiveData"
import PlaceModel from "../../Place/model"
import * as hotScenes from "../../RealmProvider/utils"
import * as sceneStats from "../../SceneStats/utils"
import { getMapPlaces } from "./getMapPlaces"
import * as worldsUtils from "../../World/utils"
import { getAllPlacesList } from "./getAllPlacesList"

const find = jest.spyOn(PlaceModel, "namedQuery")
const catalystHotScenes = jest.spyOn(hotScenes, "getHotScenes")
const catalystSceneStats = jest.spyOn(sceneStats, "getSceneStats")
const worldsContentServerLiveData = jest.spyOn(worldsUtils, "getWorldsLiveData")

afterEach(() => {
find.mockReset()
catalystHotScenes.mockReset()
catalystSceneStats.mockReset()
worldsContentServerLiveData.mockReset()
})

test("should return a object of places with no query", async () => {
find.mockResolvedValueOnce(
Promise.resolve([placeGenesisPlazaWithAggregatedAttributes])
)
find.mockResolvedValueOnce(Promise.resolve([{ total: 1 }]))
test("should return a list of places with no query", async () => {
find.mockResolvedValueOnce(Promise.resolve(allPlacesWithAggregatedAttributes))
find.mockResolvedValueOnce(Promise.resolve([{ total: 2 }]))
catalystHotScenes.mockReturnValueOnce([hotSceneGenesisPlaza])
catalystSceneStats.mockResolvedValueOnce(
Promise.resolve(sceneStatsGenesisPlaza)
)
worldsContentServerLiveData.mockReturnValueOnce(worldsLiveData)
const request = new Request("/")
const url = new URL("https://localhost/")
const placeResponse = await getMapPlaces({
const placeResponse = await getAllPlacesList({
request,
url,
})
expect(placeResponse.body).toEqual({
ok: true,
total: 1,
data: {
["-9,-9"]: {
...placeGenesisPlazaWithAggregatedAttributes,
total: 2,
data: [
{
...allPlacesWithAggregatedAttributes[0],
user_count: hotSceneGenesisPlaza.usersTotalCount,
user_visits: sceneStatsGenesisPlaza["-9,-9"].last_30d.users,
positions: undefined,
},
},
{
...allPlacesWithAggregatedAttributes[1],
user_count: worldsLiveData.perWorld[0].users,
// TODO: Get user visits from world stats
user_visits: 0,
},
],
})
expect(find.mock.calls.length).toBe(2)
expect(catalystHotScenes.mock.calls.length).toBe(1)
expect(catalystSceneStats.mock.calls.length).toBe(1)
expect(worldsContentServerLiveData.mock.calls.length).toBe(1)
})

test("should return a object of places with query", async () => {
test("should return a list of places with query", async () => {
find.mockResolvedValueOnce(
Promise.resolve([placeGenesisPlazaWithAggregatedAttributes])
Promise.resolve(allPlacesWithAggregatedAttributes.slice(0, 1))
)
find.mockResolvedValueOnce(Promise.resolve([{ total: 1 }]))
catalystHotScenes.mockReturnValueOnce([hotSceneGenesisPlaza])

catalystSceneStats.mockResolvedValueOnce(
Promise.resolve(sceneStatsGenesisPlaza)
)
worldsContentServerLiveData.mockReturnValueOnce(worldsLiveData)
const request = new Request("/")
const url = new URL(
"https://localhost/?position=-9,-9&limit=1&offset=1&order_by=like_rate&order=asc"
)
const placeResponse = await getMapPlaces({
const placeResponse = await getAllPlacesList({
request,
url,
})

expect(placeResponse.body).toEqual({
ok: true,
total: 1,
data: {
["-9,-9"]: {
...placeGenesisPlazaWithAggregatedAttributes,
data: [
{
...allPlacesWithAggregatedAttributes[0],
user_count: hotSceneGenesisPlaza.usersTotalCount,
user_visits: sceneStatsGenesisPlaza["-9,-9"].last_30d.users,
positions: undefined,
},
},
],
})
expect(find.mock.calls.length).toBe(2)
expect(catalystHotScenes.mock.calls.length).toBe(1)
expect(catalystSceneStats.mock.calls.length).toBe(1)
expect(worldsContentServerLiveData.mock.calls.length).toBe(1)
})

test("should return a object of places with order by most_active", async () => {
find.mockResolvedValueOnce(
Promise.resolve([placeGenesisPlazaWithAggregatedAttributes])
)
test("should return a list of places with order by most_active", async () => {
find.mockResolvedValueOnce(Promise.resolve(allPlacesWithAggregatedAttributes))
catalystHotScenes.mockReturnValueOnce([hotSceneGenesisPlaza])

catalystSceneStats.mockResolvedValueOnce(
Promise.resolve(sceneStatsGenesisPlaza)
)
worldsContentServerLiveData.mockReturnValueOnce(worldsLiveData)
const request = new Request("/")
const url = new URL("https://localhost/?&order_by=most_active&limit=1")
const placeResponse = await getMapPlaces({
const url = new URL("https://localhost/?&order_by=most_active")
const placeResponse = await getAllPlacesList({
request,
url,
})

expect(placeResponse.body).toEqual({
ok: true,
total: 1,
data: {
["-9,-9"]: {
...placeGenesisPlazaWithAggregatedAttributes,
total: 2,
data: [
{
...allPlacesWithAggregatedAttributes[1],
user_count: worldsLiveData.perWorld[0].users,
// TODO: Get user visits from world stats
user_visits: 0,
},
{
...allPlacesWithAggregatedAttributes[0],
user_count: hotSceneGenesisPlaza.usersTotalCount,
user_visits: sceneStatsGenesisPlaza["-9,-9"].last_30d.users,
positions: undefined,
},
},
],
})
expect(find.mock.calls.length).toBe(1)
expect(catalystHotScenes.mock.calls.length).toBe(1)
expect(catalystSceneStats.mock.calls.length).toBe(1)
expect(worldsContentServerLiveData.mock.calls.length).toBe(1)
})

test("should return a object of places with Realm details", async () => {
find.mockResolvedValueOnce(
Promise.resolve([placeGenesisPlazaWithAggregatedAttributes])
)
find.mockResolvedValueOnce(Promise.resolve([{ total: 1 }]))
test("should return a list of places with Realm details", async () => {
find.mockResolvedValueOnce(Promise.resolve(allPlacesWithAggregatedAttributes))
find.mockResolvedValueOnce(Promise.resolve([{ total: 2 }]))
catalystHotScenes.mockReturnValueOnce([hotSceneGenesisPlaza])

catalystSceneStats.mockResolvedValueOnce(
Promise.resolve(sceneStatsGenesisPlaza)
)
worldsContentServerLiveData.mockReturnValueOnce(worldsLiveData)
const request = new Request("/")
const url = new URL("https://localhost/?with_realms_detail=true")
const placeResponse = await getMapPlaces({
const placeResponse = await getAllPlacesList({
request,
url,
})
expect(placeResponse.body).toEqual({
ok: true,
total: 1,
data: {
["-9,-9"]: {
...placeGenesisPlazaWithAggregatedAttributes,
total: 2,
data: [
{
...allPlacesWithAggregatedAttributes[0],
user_count: hotSceneGenesisPlaza.usersTotalCount,
user_visits: sceneStatsGenesisPlaza["-9,-9"].last_30d.users,
realms_detail: hotSceneGenesisPlaza.realms,
positions: undefined,
},
},
{
...allPlacesWithAggregatedAttributes[1],
user_count: worldsLiveData.perWorld[0].users,
// TODO: Get user visits from world stats
user_visits: 0,
},
],
})
expect(find.mock.calls.length).toBe(2)
expect(catalystHotScenes.mock.calls.length).toBe(1)
expect(catalystSceneStats.mock.calls.length).toBe(1)
expect(worldsContentServerLiveData.mock.calls.length).toBe(1)
})

test("should return 0 as total list when query onlyFavorites with no auth", async () => {
const request = new Request("/")
const url = new URL("https://localhost/?only_favorites=true")
const placeResponse = await getMapPlaces({
const placeResponse = await getAllPlacesList({
request,
url,
})
Expand All @@ -174,7 +191,7 @@ test("should return an error when a wrong value has been sent in the query", asy
const url = new URL("https://localhost/?order_by=fake")

expect(async () =>
getMapPlaces({
getAllPlacesList({
request,
url,
})
Expand Down
99 changes: 99 additions & 0 deletions src/entities/Map/routes/getAllPlacesList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { withAuthOptional } from "decentraland-gatsby/dist/entities/Auth/routes/withDecentralandAuth"
import Context from "decentraland-gatsby/dist/entities/Route/wkc/context/Context"
import ApiResponse from "decentraland-gatsby/dist/entities/Route/wkc/response/ApiResponse"
import Router from "decentraland-gatsby/dist/entities/Route/wkc/routes/Router"
import {
bool,
numeric,
oneOf,
} from "decentraland-gatsby/dist/entities/Schema/utils"

import PlaceModel from "../../Place/model"
import { PlaceListOrderBy } from "../../Place/types"
import { getHotScenes } from "../../RealmProvider/utils"
import { getSceneStats } from "../../SceneStats/utils"
import { getWorldsLiveData } from "../../World/utils"
import { getAllPlacesListQuerySchema } from "../schemas"
import {
DEFAULT_MAX_LIMIT,
FindAllPlacesWithAggregatesOptions,
GetAllPlaceListQuery,
} from "../types"
import { allPlacesWithAggregates } from "../utils"
import { getAllPlacesMostActiveList } from "./getAllPlacesMostActiveList"

export const validateGetPlaceListQuery = Router.validator<GetAllPlaceListQuery>(
getAllPlacesListQuerySchema
)

export const getAllPlacesList = Router.memo(
async (ctx: Context<{}, "url" | "request">) => {
if (ctx.url.searchParams.get("order_by") === PlaceListOrderBy.MOST_ACTIVE) {
return getAllPlacesMostActiveList(ctx)
}

const query = await validateGetPlaceListQuery({
offset: ctx.url.searchParams.get("offset"),
limit: ctx.url.searchParams.get("limit"),
only_favorites: ctx.url.searchParams.get("only_favorites"),
only_featured: ctx.url.searchParams.get("only_featured"),
only_highlighted: ctx.url.searchParams.get("only_highlighted"),
positions: ctx.url.searchParams.getAll("positions"),
names: ctx.url.searchParams.getAll("names"),
order_by:
oneOf(ctx.url.searchParams.get("order_by"), [
PlaceListOrderBy.LIKE_SCORE_BEST,
PlaceListOrderBy.UPDATED_AT,
PlaceListOrderBy.CREATED_AT,
]) || PlaceListOrderBy.LIKE_SCORE_BEST,
order:
oneOf(ctx.url.searchParams.get("order"), ["asc", "desc"]) || "desc",
with_realms_detail: ctx.url.searchParams.get("with_realms_detail"),
search: ctx.url.searchParams.get("search"),
categories: ctx.url.searchParams.getAll("categories"),
})

const userAuth = await withAuthOptional(ctx)

if (bool(query.only_favorites) && !userAuth?.address) {
return new ApiResponse([], { total: 0 })
}

const options: FindAllPlacesWithAggregatesOptions = {
user: userAuth?.address,
offset: numeric(query.offset, { min: 0 }) ?? 0,
limit:
numeric(query.limit, { min: 0, max: DEFAULT_MAX_LIMIT }) ??
DEFAULT_MAX_LIMIT,
only_favorites: !!bool(query.only_favorites),
only_highlighted: !!bool(query.only_highlighted),
positions: query.positions,
names: query.names,
order_by: query.order_by,
order: query.order,
search: query.search,
categories: query.categories,
}

const hotScenes = getHotScenes()
const worldsLiveData = getWorldsLiveData()

const [data, total, sceneStats] = await Promise.all([
PlaceModel.findAllPlacesWithAggregates(options),
PlaceModel.countAllPlaces(options),
getSceneStats(),
])

const response = allPlacesWithAggregates(
data,
hotScenes,
sceneStats,
worldsLiveData,
{
withRealmsDetail: !!bool(query.with_realms_detail),
}
)

return new ApiResponse(response, { total })
}
)
Loading

0 comments on commit 0533ea4

Please sign in to comment.