Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6068086
modify tests to check the whole response body
tudddorrr Dec 21, 2021
2a5eb03
add missing migration for cascade delete player events
tudddorrr Dec 21, 2021
1784ff0
Merge pull request #48 from TaloDev/check-whole-response
tudddorrr Dec 21, 2021
ce36252
Merge pull request #49 from TaloDev/events-delete-cascade-migration
tudddorrr Dec 21, 2021
c4320bb
rename migration files to include what each did
tudddorrr Dec 21, 2021
3f8784c
update migrations contributing docs
tudddorrr Dec 21, 2021
c9c4933
Merge pull request #50 from TaloDev/named-migrations
tudddorrr Dec 21, 2021
c794fc8
add extra flag when 2fa session has expired
tudddorrr Dec 21, 2021
ab45dea
add leaderboard entries to available exportable entities
tudddorrr Dec 22, 2021
6b0be2c
Merge pull request #51 from TaloDev/export-leaderboard-entries
tudddorrr Dec 22, 2021
a49e284
leaderboard entries can be hidden/unhidden
tudddorrr Dec 24, 2021
a3c7fc5
Merge pull request #52 from TaloDev/hideable-leaderboard-entries
tudddorrr Dec 27, 2021
fdff62c
flush updated leaderboard entry
tudddorrr Dec 28, 2021
cf53532
move gameId key to body for updating a leaderboard entry
tudddorrr Dec 31, 2021
971c894
update leaderboard entry patch tests
tudddorrr Dec 31, 2021
ea44250
add endpoint for updating leaderboards
tudddorrr Jan 1, 2022
4b411d2
Merge pull request #53 from TaloDev/update-leaderboards
tudddorrr Jan 2, 2022
a568edd
filter out hidden leaderboard entries from api requests
tudddorrr Jan 3, 2022
5cc6ecc
Merge pull request #55 from TaloDev/filter-hidden-leaderboard-entries
tudddorrr Jan 3, 2022
4424a33
0.3.0
tudddorrr Jan 3, 2022
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
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ This will create a policy, entity and REST API for your new entity. If you want

## Migrations

To create a migration, use `yarn migration:create`. This will create a migration class in the `migrations` folder. You will then need to import that migration class into the `index.ts` in the same folder.
To create a migration, use `yarn migration:create`. This will create a migration class in the `migrations` folder.

Modify the default name of the file from `Migration[Timestamp].ts` to `[Timestamp][PascalCaseDescriptionOfTheMigration].ts`.

You should also rename the exported class to be `[PascalCaseDescriptionOfTheMigration]`.

You will then need to import and add that migration class to the end of the list of migrations inside `index.ts` in the same folder.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "game-services",
"version": "0.2.0",
"version": "0.3.0",
"description": "",
"main": "src/index.ts",
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion src/entities/data-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export enum DataExportStatus {
export enum DataExportAvailableEntities {
EVENTS = 'events',
PLAYERS = 'players',
PLAYER_ALIASES = 'playerAliases'
PLAYER_ALIASES = 'playerAliases',
LEADERBOARD_ENTRIES = 'leaderboardEntries'
}

@Entity()
Expand Down
4 changes: 4 additions & 0 deletions src/entities/leaderboard-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default class LeaderboardEntry {
@ManyToOne(() => PlayerAlias, { cascade: [Cascade.REMOVE], eager: true })
playerAlias: PlayerAlias

@Property({ default: false })
hidden: boolean

@Property()
createdAt: Date = new Date()

Expand All @@ -36,6 +39,7 @@ export default class LeaderboardEntry {
service: this.playerAlias.service,
identifier: this.playerAlias.identifier
},
hidden: this.hidden,
updatedAt: this.updatedAt
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/entities/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export default class Leaderboard {
name: this.name,
sortMode: this.sortMode,
unique: this.unique,
createdAt: this.createdAt
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'

export class Migration20210725211129 extends Migration {
export class InitialMigration extends Migration {

async up(): Promise<void> {
this.addSql('create table `organisation` (`id` int unsigned not null auto_increment primary key, `email` varchar(255) not null, `name` varchar(255) not null, `created_at` datetime not null, `updated_at` datetime not null) default character set utf8mb4 engine = InnoDB')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'

export class Migration20210926160859 extends Migration {
export class CreateDataExportsTable extends Migration {

async up(): Promise<void> {
this.addSql('create table `data_export` (`id` int unsigned not null auto_increment primary key, `created_by_user_id` int(11) unsigned not null, `game_id` int(11) unsigned not null, `entities` text not null, `status` tinyint not null, `failed_at` datetime null, `created_at` datetime not null, `updated_at` datetime not null) default character set utf8mb4 engine = InnoDB;')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'

export class Migration20211107233610 extends Migration {
export class CreateLeaderboardsTable extends Migration {

async up(): Promise<void> {
this.addSql('create table `leaderboard` (`id` int unsigned not null auto_increment primary key, `internal_name` varchar(255) not null, `name` varchar(255) not null, `sort_mode` enum(\'desc\', \'asc\') not null, `unique` tinyint(1) not null, `game_id` int(11) unsigned not null, `created_at` datetime not null, `updated_at` datetime not null) default character set utf8mb4 engine = InnoDB;')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'

export class Migration20211205171927 extends Migration {
export class CreateUserTwoFactorAuthTable extends Migration {

async up(): Promise<void> {
this.addSql('create table `user_two_factor_auth` (`id` int unsigned not null auto_increment primary key, `secret` varchar(255) not null, `enabled` tinyint(1) not null) default character set utf8mb4 engine = InnoDB;')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Migration } from '@mikro-orm/migrations'

export class Migration20211209003017 extends Migration {
export class CreateUserRecoveryCodeTable extends Migration {

async up(): Promise<void> {
this.addSql('create table `user_recovery_code` (`id` int unsigned not null auto_increment primary key, `user_id` int(11) unsigned not null, `code` varchar(255) not null, `created_at` datetime not null) default character set utf8mb4 engine = InnoDB;')
Expand Down
12 changes: 12 additions & 0 deletions src/migrations/20211221195514CascadeDeletePlayerAliasEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Migration } from '@mikro-orm/migrations'

export class CascadeDeletePlayerAliasEvents extends Migration {

async up(): Promise<void> {
this.addSql('alter table `event` modify `player_alias_id` int(11) unsigned null;')

this.addSql('alter table `event` drop foreign key `event_player_alias_id_foreign`;')
this.addSql('alter table `event` add constraint `event_player_alias_id_foreign` foreign key (`player_alias_id`) references `player_alias` (`id`) on delete cascade;')
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Migration } from '@mikro-orm/migrations'

export class AddLeaderboardEntryHiddenColumn extends Migration {

async up(): Promise<void> {
this.addSql('alter table `leaderboard_entry` add `hidden` tinyint(1) not null default false;')
}

}
40 changes: 25 additions & 15 deletions src/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import { Migration20210725211129 } from './Migration20210725211129'
import { Migration20210926160859 } from './Migration20210926160859'
import { Migration20211107233610 } from './Migration20211107233610'
import { Migration20211205171927 } from './Migration20211205171927'
import { Migration20211209003017 } from './Migration20211209003017'
import { InitialMigration } from './20210725211129InitialMigration'
import { CreateDataExportsTable } from './20210926160859CreateDataExportsTable'
import { CreateLeaderboardsTable } from './20211107233610CreateLeaderboardsTable'
import { CreateUserTwoFactorAuthTable } from './20211205171927CreateUserTwoFactorAuthTable'
import { CreateUserRecoveryCodeTable } from './20211209003017CreateUserRecoveryCodeTable'
import { CascadeDeletePlayerAliasEvents } from './20211221195514CascadeDeletePlayerAliasEvents'
import { AddLeaderboardEntryHiddenColumn } from './20211224154919AddLeaderboardEntryHiddenColumn'

export default [
{
name: 'Migration20210725211129',
class: Migration20210725211129
name: 'InitialMigration',
class: InitialMigration
},
{
name: 'Migration20210926160859',
class: Migration20210926160859
name: 'CreateDataExportsTable',
class: CreateDataExportsTable
},
{
name: 'Migration20211107233610',
class: Migration20211107233610
name: 'CreateLeaderboardsTable',
class: CreateLeaderboardsTable
},
{
name: 'Migration20211205171927',
class: Migration20211205171927
name: 'CreateUserTwoFactorAuthTable',
class: CreateUserTwoFactorAuthTable
},
{
name: 'Migration20211209003017',
class: Migration20211209003017
name: 'CreateUserRecoveryCodeTable',
class: CreateUserRecoveryCodeTable
},
{
name: 'CascadeDeletePlayerAliasEvents',
class: CascadeDeletePlayerAliasEvents
},
{
name: 'AddLeaderboardEntryHiddenColumn',
class: AddLeaderboardEntryHiddenColumn
}
]
18 changes: 18 additions & 0 deletions src/policies/leaderboards.policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,22 @@ export default class LeaderboardsPolicy extends Policy {

return await this.canAccessGame(gameId)
}

async updateEntry(req: ServiceRequest): Promise<ServicePolicyResponse> {
return await this.get({
...req,
query: {
gameId: req.body.gameId
}
})
}

async updateLeaderboard(req: ServiceRequest): Promise<ServicePolicyResponse> {
return await this.get({
...req,
query: {
gameId: req.body.gameId
}
})
}
}
18 changes: 14 additions & 4 deletions src/services/data-exports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ormConfig from '../config/mikro-orm.config'
import { EmailConfig } from '../lib/messaging/sendEmail'
import { unlink } from 'fs/promises'
import dataExportReady from '../emails/data-export-ready'
import LeaderboardEntry from '../entities/leaderboard-entry'

interface EntityWithProps {
props: Prop[]
Expand All @@ -28,7 +29,7 @@ interface DataExportJob {
dataExportId: number
}

type ExportableEntity = Event | Player | PlayerAlias
type ExportableEntity = Event | Player | PlayerAlias | LeaderboardEntry
type ExportableEntityWithProps = ExportableEntity & EntityWithProps

@Routes([
Expand Down Expand Up @@ -124,19 +125,26 @@ export default class DataExportsService implements Service {

if (dataExport.entities.includes(DataExportAvailableEntities.EVENTS)) {
const events = await em.getRepository(Event).find({ game: dataExport.game }, ['playerAlias'])
zip.addFile('events.csv', this.buildCSV(DataExportAvailableEntities.EVENTS, events))
zip.addFile(`${DataExportAvailableEntities.EVENTS}.csv`, this.buildCSV(DataExportAvailableEntities.EVENTS, events))
}

if (dataExport.entities.includes(DataExportAvailableEntities.PLAYERS)) {
const players = await em.getRepository(Player).find({ game: dataExport.game })
zip.addFile('players.csv', this.buildCSV(DataExportAvailableEntities.PLAYERS, players))
zip.addFile(`${DataExportAvailableEntities.PLAYERS}.csv`, this.buildCSV(DataExportAvailableEntities.PLAYERS, players))
}

if (dataExport.entities.includes(DataExportAvailableEntities.PLAYER_ALIASES)) {
const aliases = await em.getRepository(PlayerAlias).find({
player: { game: dataExport.game }
})
zip.addFile('player-aliases.csv', this.buildCSV(DataExportAvailableEntities.PLAYER_ALIASES, aliases))
zip.addFile(`${DataExportAvailableEntities.PLAYER_ALIASES}.csv`, this.buildCSV(DataExportAvailableEntities.PLAYER_ALIASES, aliases))
}

if (dataExport.entities.includes(DataExportAvailableEntities.LEADERBOARD_ENTRIES)) {
const entries = await em.getRepository(LeaderboardEntry).find({
leaderboard: { game: dataExport.game }
}, ['leaderboard'])
zip.addFile(`${DataExportAvailableEntities.LEADERBOARD_ENTRIES}.csv`, this.buildCSV(DataExportAvailableEntities.LEADERBOARD_ENTRIES, entries))
}

return zip
Expand All @@ -150,6 +158,8 @@ export default class DataExportsService implements Service {
return ['id', 'lastSeenAt', 'createdAt', 'updatedAt', 'props']
case DataExportAvailableEntities.PLAYER_ALIASES:
return ['id', 'service', 'identifier', 'player.id', 'createdAt', 'updatedAt']
case DataExportAvailableEntities.LEADERBOARD_ENTRIES:
return ['id', 'score', 'leaderboard.id', 'leaderboard.internalName', 'playerAlias.id', 'playerAlias.service', 'playerAlias.identifier', 'playerAlias.player.id', 'createdAt', 'updatedAt']
}
}

Expand Down
70 changes: 68 additions & 2 deletions src/services/leaderboards.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import LeaderboardsPolicy from '../policies/leaderboards.policy'
method: 'GET',
path: '/:internalName/entries',
handler: 'entries'
},
{
method: 'PATCH',
path: '/:internalName/entries/:id',
handler: 'updateEntry'
},
{
method: 'PATCH',
path: '/:internalName',
handler: 'updateLeaderboard'
}
])
export default class LeaderboardsService implements Service {
Expand Down Expand Up @@ -112,8 +122,11 @@ export default class LeaderboardsService implements Service {
.where({ leaderboard })

if (aliasId) {
baseQuery = baseQuery
.where({ playerAlias: Number(aliasId) })
baseQuery = baseQuery.andWhere({ playerAlias: Number(aliasId) })
}

if (req.ctx.state.user.api === true) {
baseQuery = baseQuery.andWhere({ hidden: false })
}

const { count } = await baseQuery
Expand All @@ -135,4 +148,57 @@ export default class LeaderboardsService implements Service {
}
}
}

@Validate({
body: ['gameId']
})
@HasPermission(LeaderboardsPolicy, 'updateEntry')
async updateEntry(req: ServiceRequest): Promise<ServiceResponse> {
const { id } = req.params
const em: EntityManager = req.ctx.em

const entry = await em.getRepository(LeaderboardEntry).findOne(Number(id))
if (!entry) {
req.ctx.throw(404, 'Leaderboard entry not found')
}

const { hidden } = req.body

if (typeof hidden === 'boolean') {
entry.hidden = hidden
}

await em.flush()

return {
status: 200,
body: {
entry
}
}
}

@Validate({
body: ['gameId']
})
@HasPermission(LeaderboardsPolicy, 'updateLeaderboard')
async updateLeaderboard(req: ServiceRequest): Promise<ServiceResponse> {
const em: EntityManager = req.ctx.em

const { name, sortMode, unique } = req.body
const leaderboard = req.ctx.state.leaderboard

if (name) leaderboard.name = name
if (sortMode) leaderboard.sortMode = sortMode
if (typeof unique === 'boolean') leaderboard.unique = unique

await em.flush()

return {
status: 200,
body: {
leaderboard
}
}
}
}
4 changes: 2 additions & 2 deletions src/services/public/users-public.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export default class UsersPublicService implements Service {
const hasSession = (await redis.get(`2fa:${user.id}`)) === 'true'

if (!hasSession) {
req.ctx.throw(403, 'Session expired')
req.ctx.throw(403, { message: 'Session expired', sessionExpired: true })
}

if (!authenticator.check(code, user.twoFactorAuth.secret)) {
Expand Down Expand Up @@ -305,7 +305,7 @@ export default class UsersPublicService implements Service {
const hasSession = (await redis.get(`2fa:${user.id}`)) === 'true'

if (!hasSession) {
req.ctx.throw(403, 'Session expired')
req.ctx.throw(403, { message: 'Session expired', sessionExpired: true })
}

const recoveryCode = user.recoveryCodes.getItems().find((recoveryCode) => {
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/LeaderboardEntryFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class LeaderboardEntryFactory extends Factory<LeaderboardEntry> {
constructor(leaderboard: Leaderboard, availablePlayers: Player[]) {
super(LeaderboardEntry, 'base')
this.register('base', this.base)
this.register('hidden', this.hidden)

this.leaderboard = leaderboard
this.availablePlayers = availablePlayers
Expand All @@ -26,4 +27,10 @@ export default class LeaderboardEntryFactory extends Factory<LeaderboardEntry> {
score: Number(casual.double(10, 100000).toFixed(2))
}
}

protected hidden(): Partial<LeaderboardEntry> {
return {
hidden: true
}
}
}
2 changes: 1 addition & 1 deletion tests/fixtures/LeaderboardFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class LeaderboardFactory extends Factory<Leaderboard> {
game,
internalName: casual.word,
name: casual.title,
sortMode: casual.random_element(Object.keys(LeaderboardSortMode)),
sortMode: casual.random_element([LeaderboardSortMode.ASC, LeaderboardSortMode.DESC]),
unique: casual.boolean,
entries: new Collection<LeaderboardEntry>(leaderboard, entries)
}
Expand Down
2 changes: 1 addition & 1 deletion tests/services/_api/events-api/post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('Events API service - post', () => {
.auth(token, { type: 'bearer' })
.expect(400)

expect(res.body.message).toBe('Events must be an array')
expect(res.body).toStrictEqual({ message: 'Events must be an array' })
})

it('should sanitise event props into strings', async () => {
Expand Down
Loading