Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions open-api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,18 @@
"required": ["params", "results"],
"additionalProperties": false
},
"PlayerBasicBatchResponse": {
"type": "object",
"properties": {
"players": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PlayerInfo"
}
}
},
"required": ["players"]
},
"PlayerHistoryResponse": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -4169,6 +4181,144 @@
}
}
},
"/player/basic/batch": {
"post": {
"description": "Batch variant of `/player/{membershipId}/basic`. Resolves up to 12 players in one round-trip.",
"summary": "/player/basic/batch",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"membershipIds": {
"type": "array",
"items": {
"type": "string",
"pattern": "^\\d+n?$"
},
"minItems": 1,
"maxItems": 12
}
},
"required": ["membershipIds"]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"minted": {
"type": "string",
"format": "date-time"
},
"success": {
"type": "boolean",
"enum": [true]
},
"response": {
"$ref": "#/components/schemas/PlayerBasicBatchResponse"
}
},
"required": ["minted", "success", "response"]
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"minted": {
"type": "string",
"format": "date-time"
},
"success": {
"type": "boolean",
"enum": [false]
},
"code": {
"type": "string",
"enum": ["BodyValidationError"]
},
"error": {
"$ref": "#/components/schemas/BodyValidationError"
}
},
"required": ["minted", "success", "code", "error"]
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"minted": {
"type": "string",
"format": "date-time"
},
"success": {
"type": "boolean",
"enum": [false]
},
"code": {
"type": "string",
"enum": ["ApiKeyError"]
},
"error": {
"$ref": "#/components/schemas/ApiKeyError"
}
},
"required": ["minted", "success", "code", "error"]
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"minted": {
"type": "string",
"format": "date-time"
},
"success": {
"type": "boolean",
"enum": [false]
},
"code": {
"type": "string",
"enum": ["InternalServerError"]
},
"error": {
"$ref": "#/components/schemas/InternalServerError"
}
},
"required": ["minted", "success", "code", "error"]
}
}
}
}
}
}
},
"/player/{membershipId}/history": {
"get": {
"description": "/activities is deprecated. Use /history now. Get a player's activity history. This endpoint uses date cursors to paginate through a player's activity history. \nThe first request should not include a cursor. Subsequent requests should include the `nextCursor` \nvalue from the previous response. Note that the first request may not return the full number of activities requested\nin order to optimize performance. Subsequent requests will return the full number of activities requested.",
Expand Down
53 changes: 53 additions & 0 deletions src/routes/player/basic-batch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"

import { getFixturePool } from "@/lib/test-fixture-db"
import { expectOk } from "@/lib/test-utils"

import { playerBasicBatchRoute } from "./basic-batch"

const fixtureDb = getFixturePool()
const fixtureMembershipId = "4611686019000000402"

beforeAll(async () => {
await fixtureDb.query(`DELETE FROM core.player_stats WHERE membership_id = $1::bigint`, [
fixtureMembershipId
])
await fixtureDb.query(`DELETE FROM core.player WHERE membership_id = $1::bigint`, [
fixtureMembershipId
])
await fixtureDb.query(
`INSERT INTO core.player (
membership_id, membership_type, icon_path, display_name,
bungie_global_display_name, bungie_global_display_name_code, last_seen, first_seen,
clears, fresh_clears, sherpas, total_time_played_seconds, sum_of_best, wfr_score,
cheat_level, is_private, is_whitelisted, updated_at
) VALUES
($1::bigint, 3, NULL, 'fixture_basic_batch', 'fixture_basic_batch', '0402', NOW(), NOW(), 1, 1, 0, 100, 100, 0, 0, false, false, NOW())`,
[fixtureMembershipId]
)
})

afterAll(async () => {
await fixtureDb.query(`DELETE FROM core.player_stats WHERE membership_id = $1::bigint`, [
fixtureMembershipId
])
await fixtureDb.query(`DELETE FROM core.player WHERE membership_id = $1::bigint`, [
fixtureMembershipId
])
})

describe("player basic batch 200", () => {
test("returns found players and omits unknown ids", async () => {
const result = await playerBasicBatchRoute.$mock({
body: {
membershipIds: [fixtureMembershipId, "1"]
}
})

expectOk(result)
if (result.type === "ok") {
expect(result.parsed.players).toHaveLength(1)
expect(result.parsed.players[0]?.membershipId).toBe(BigInt(fixtureMembershipId))
}
})
})
35 changes: 35 additions & 0 deletions src/routes/player/basic-batch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { RaidHubRoute } from "@/core/RaidHubRoute"
import { playersQueue } from "@/integrations/rabbitmq/queues"
import { cacheControl } from "@/middleware/cache-control"
import { zPlayerInfo } from "@/schema/components/PlayerInfo"
import { zBigIntString } from "@/schema/input"
import { getPlayers } from "@/services/player"
import { z } from "zod"

export const playerBasicBatchRoute = new RaidHubRoute({
method: "post",
description:
"Batch variant of `/player/{membershipId}/basic`. Resolves up to 12 players in one round-trip.",
body: z.object({
membershipIds: z.array(zBigIntString()).min(1).max(12)
}),
middleware: [cacheControl(300)],
response: {
success: {
statusCode: 200,
schema: z.object({
players: z.array(zPlayerInfo)
})
},
errors: []
},
async handler(req, after) {
const players = await getPlayers(req.body.membershipIds)

after(async () => {
await Promise.all(req.body.membershipIds.map(id => playersQueue.send(id)))
})

return RaidHubRoute.ok({ players })
}
})
2 changes: 2 additions & 0 deletions src/routes/player/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RaidHubRouter } from "@/core/RaidHubRouter"
import { playerBasicBatchRoute } from "./basic-batch"
import { playerBasicRoute } from "./membershipId/basic"
import { playerHistoryRoute } from "./membershipId/history"
import { playerInstancesRoute } from "./membershipId/instances"
Expand All @@ -9,6 +10,7 @@ import { playerSearchRoute } from "./search"
export const playerRouter = new RaidHubRouter({
routes: [
{ path: "/search", route: playerSearchRoute },
{ path: "/basic/batch", route: playerBasicBatchRoute },
{
path: "/:membershipId",
route: new RaidHubRouter({
Expand Down
22 changes: 22 additions & 0 deletions src/services/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@
{ params: [membershipId] }
)
}

export const getPlayers = async (membershipIds: readonly (bigint | string)[]) => {
if (!membershipIds.length) {
return []

Check warning on line 32 in src/services/player.ts

View workflow job for this annotation

GitHub Actions / test

32 line is not covered with tests
}

return await pgReader.queryRows<PlayerInfo>(
`SELECT
membership_id AS "membershipId",
membership_type AS "membershipType",
icon_path AS "iconPath",
display_name AS "displayName",
bungie_global_display_name AS "bungieGlobalDisplayName",
bungie_global_display_name_code AS "bungieGlobalDisplayNameCode",
last_seen AS "lastSeen",
is_private AS "isPrivate",
cheat_level AS "cheatLevel"
FROM player
WHERE membership_id = ANY($1::bigint[])`,
{ params: [membershipIds] }
)
}
export const getPlayerActivityStats = async (membershipId: bigint | string) => {
return await withHistogramTimer(
playerProfileQueryTimer,
Expand Down
Loading