Skip to content

Commit 3d64a69

Browse files
committed
add support for props in leaderboard entries
1 parent 50b783d commit 3d64a69

File tree

6 files changed

+119
-5
lines changed

6 files changed

+119
-5
lines changed

src/entities/leaderboard-entry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Cascade, Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/mysql'
1+
import { Cascade, Embedded, Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/mysql'
22
import Leaderboard from './leaderboard'
33
import PlayerAlias from './player-alias'
4+
import Prop from './prop'
45

56
@Entity()
67
export default class LeaderboardEntry {
@@ -16,6 +17,9 @@ export default class LeaderboardEntry {
1617
@ManyToOne(() => PlayerAlias, { cascade: [Cascade.REMOVE], eager: true })
1718
playerAlias: PlayerAlias
1819

20+
@Embedded(() => Prop, { array: true })
21+
props: Prop[] = []
22+
1923
@Property({ default: false })
2024
hidden: boolean
2125

@@ -37,6 +41,7 @@ export default class LeaderboardEntry {
3741
leaderboardInternalName: this.leaderboard.internalName,
3842
playerAlias: this.playerAlias,
3943
hidden: this.hidden,
44+
props: this.props,
4045
createdAt: this.createdAt,
4146
updatedAt: this.updatedAt
4247
}

src/migrations/.snapshot-gs_dev.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,6 +1662,16 @@
16621662
"length": null,
16631663
"mappedType": "integer"
16641664
},
1665+
"props": {
1666+
"name": "props",
1667+
"type": "json",
1668+
"unsigned": false,
1669+
"autoincrement": false,
1670+
"primary": false,
1671+
"nullable": false,
1672+
"length": null,
1673+
"mappedType": "json"
1674+
},
16651675
"hidden": {
16661676
"name": "hidden",
16671677
"type": "tinyint(1)",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Migration } from '@mikro-orm/migrations'
2+
3+
export class AddLeaderboardEntryPropsColumn extends Migration {
4+
5+
override async up(): Promise<void> {
6+
this.addSql('alter table `leaderboard_entry` add `props` json not null;')
7+
}
8+
9+
override async down(): Promise<void> {
10+
this.addSql('alter table `leaderboard_entry` drop column `props`;')
11+
}
12+
13+
}

src/migrations/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { CreatePlayerAuthTable } from './20240628155142CreatePlayerAuthTable'
2828
import { CreatePlayerAuthActivityTable } from './20240725183402CreatePlayerAuthActivityTable'
2929
import { UpdatePlayerAliasServiceColumn } from './20240916213402UpdatePlayerAliasServiceColumn'
3030
import { AddPlayerAliasAnonymisedColumn } from './20240920121232AddPlayerAliasAnonymisedColumn'
31+
import { AddLeaderboardEntryPropsColumn } from './20240922222426AddLeaderboardEntryPropsColumn'
3132

3233
export default [
3334
{
@@ -149,5 +150,9 @@ export default [
149150
{
150151
name: 'AddPlayerAliasAnonymisedColumn',
151152
class: AddPlayerAliasAnonymisedColumn
153+
},
154+
{
155+
name: 'AddLeaderboardEntryPropsColumn',
156+
class: AddLeaderboardEntryPropsColumn
152157
}
153158
]

src/services/api/leaderboard-api.service.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Leaderboard, { LeaderboardSortMode } from '../../entities/leaderboard'
77
import LeaderboardAPIDocs from '../../docs/leaderboard-api.docs'
88
import triggerIntegrations from '../../lib/integrations/triggerIntegrations'
99
import { devDataPlayerFilter } from '../../middlewares/dev-data-middleware'
10+
import sanitiseProps from '../../lib/props/sanitiseProps'
11+
import { uniqWith } from 'lodash'
1012

1113
@Routes([
1214
{
@@ -31,7 +33,7 @@ export default class LeaderboardAPIService extends APIService {
3133
})
3234
}
3335

34-
async createEntry(req: Request): Promise<LeaderboardEntry> {
36+
async createEntry(req: Request, props?: { key: string, value: string }[]): Promise<LeaderboardEntry> {
3537
const em: EntityManager = req.ctx.em
3638

3739
const entry = new LeaderboardEntry(req.ctx.state.leaderboard)
@@ -40,6 +42,9 @@ export default class LeaderboardAPIService extends APIService {
4042
if (req.ctx.state.continuityDate) {
4143
entry.createdAt = req.ctx.state.continuityDate
4244
}
45+
if (props) {
46+
entry.props = sanitiseProps(props)
47+
}
4348

4449
await em.persistAndFlush(entry)
4550

@@ -53,9 +58,13 @@ export default class LeaderboardAPIService extends APIService {
5358
@HasPermission(LeaderboardAPIPolicy, 'post')
5459
@Docs(LeaderboardAPIDocs.post)
5560
async post(req: Request): Promise<Response> {
56-
const { score } = req.body
61+
const { score, props } = req.body
5762
const em: EntityManager = req.ctx.em
5863

64+
if (props && !Array.isArray(props)) {
65+
req.ctx.throw(400, 'Props must be an array')
66+
}
67+
5968
const leaderboard: Leaderboard = req.ctx.state.leaderboard
6069

6170
let entry: LeaderboardEntry = null
@@ -71,15 +80,23 @@ export default class LeaderboardAPIService extends APIService {
7180
if ((leaderboard.sortMode === LeaderboardSortMode.ASC && score < entry.score) || (leaderboard.sortMode === LeaderboardSortMode.DESC && score > entry.score)) {
7281
entry.score = score
7382
entry.createdAt = req.ctx.state.continuityDate ?? new Date()
83+
if (props) {
84+
const mergedProps = uniqWith([
85+
...sanitiseProps(props),
86+
...entry.props
87+
], (a, b) => a.key === b.key)
88+
89+
entry.props = sanitiseProps(mergedProps, true)
90+
}
7491
await em.flush()
7592

7693
updated = true
7794
}
7895
} else {
79-
entry = await this.createEntry(req)
96+
entry = await this.createEntry(req, props)
8097
}
8198
} catch (err) {
82-
entry = await this.createEntry(req)
99+
entry = await this.createEntry(req, props)
83100
}
84101

85102
await triggerIntegrations(em, leaderboard.game, (integration) => {

tests/services/_api/leaderboard-api/post.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,68 @@ describe('Leaderboard API service - post', () => {
266266

267267
expect(new Date(res.body.entry.createdAt).getHours()).toBe(continuityDate.getHours())
268268
})
269+
270+
it('should create entries with props', async () => {
271+
const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.WRITE_LEADERBOARDS])
272+
const player = await new PlayerFactory([apiKey.game]).one()
273+
const leaderboard = await new LeaderboardFactory([apiKey.game]).state(() => ({ unique: false })).one()
274+
await (<EntityManager>global.em).persistAndFlush([player, leaderboard])
275+
276+
const res = await request(global.app)
277+
.post(`/v1/leaderboards/${leaderboard.internalName}/entries`)
278+
.send({
279+
score: 300,
280+
props: [
281+
{ key: 'key1', value: 'value1' },
282+
{ key: 'key2', value: 'value2' }
283+
]
284+
})
285+
.auth(token, { type: 'bearer' })
286+
.set('x-talo-alias', String(player.aliases[0].id))
287+
.expect(200)
288+
289+
expect(res.body.entry.score).toBe(300)
290+
expect(res.body.entry.props).toStrictEqual([
291+
{ key: 'key1', value: 'value1' },
292+
{ key: 'key2', value: 'value2' }
293+
])
294+
})
295+
296+
it('should update an existing entry\'s props', async () => {
297+
const [apiKey, token] = await createAPIKeyAndToken([APIKeyScope.WRITE_LEADERBOARDS])
298+
const player = await new PlayerFactory([apiKey.game]).one()
299+
const leaderboard = await new LeaderboardFactory([apiKey.game]).state(() => ({ unique: true, sortMode: LeaderboardSortMode.DESC })).one()
300+
301+
const entry = await new LeaderboardEntryFactory(leaderboard, [player]).state(() => ({
302+
score: 100,
303+
playerAlias: player.aliases[0],
304+
props: [
305+
{ key: 'key1', value: 'value1' },
306+
{ key: 'delete-me', value: 'delete-me' }
307+
]
308+
})).one()
309+
310+
await (<EntityManager>global.em).persistAndFlush([player, leaderboard, entry])
311+
312+
const res = await request(global.app)
313+
.post(`/v1/leaderboards/${leaderboard.internalName}/entries`)
314+
.send({
315+
score: 300,
316+
props: [
317+
{ key: 'key2', value: 'value2' },
318+
{ key: 'delete-me', value: null }
319+
]
320+
})
321+
.auth(token, { type: 'bearer' })
322+
.set('x-talo-alias', String(player.aliases[0].id))
323+
.expect(200)
324+
325+
expect(res.body.entry.score).toBe(300)
326+
expect(res.body.updated).toBe(true)
327+
328+
expect(res.body.entry.props).toStrictEqual([
329+
{ key: 'key2', value: 'value2' },
330+
{ key: 'key1', value: 'value1' }
331+
])
332+
})
269333
})

0 commit comments

Comments
 (0)