Skip to content

Commit

Permalink
Merge pull request #343 from TaloDev/pinned-groups
Browse files Browse the repository at this point in the history
Pinned groups
  • Loading branch information
tudddorrr authored Oct 3, 2024
2 parents 3128e4e + 95483de commit 4cc93cc
Show file tree
Hide file tree
Showing 16 changed files with 447 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import UserPinnedGroup from './user-pinned-group'
import PlayerAuthActivity from './player-auth-activity'
import PlayerAuth from './player-auth'
import GameFeedback from './game-feedback'
Expand Down Expand Up @@ -33,6 +34,7 @@ import PlayerGroup from './player-group'
import GameSecret from './game-secret'

export default [
UserPinnedGroup,
PlayerAuthActivity,
PlayerAuth,
GameFeedback,
Expand Down
4 changes: 3 additions & 1 deletion src/entities/subscribers/player.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export default class PlayerSubscriber implements EventSubscriber {
const em = (args.em as EntityManager).fork()

const changeSets = args.uow.getChangeSets()
const cs = changeSets.find((cs) => [ChangeSetType.CREATE, ChangeSetType.UPDATE].includes(cs.type) && cs.entity instanceof Player)
const cs = changeSets.find((cs) => {
return [ChangeSetType.CREATE, ChangeSetType.UPDATE].includes(cs.type) && cs.entity instanceof Player
})

if (cs) {
await checkGroupMemberships(em, cs.entity as Player)
Expand Down
24 changes: 24 additions & 0 deletions src/entities/user-pinned-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Entity, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/mysql'
import User from './user'
import PlayerGroup from './player-group'

@Entity()
@Unique({ properties: ['user', 'group'] })
export default class UserPinnedGroup {
@PrimaryKey()
id: number

@ManyToOne(() => User, { eager: true })
user: User

@ManyToOne(() => PlayerGroup, { eager: true })
group: PlayerGroup

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

constructor(user: User, group: PlayerGroup) {
this.user = user
this.group = group
}
}
2 changes: 1 addition & 1 deletion src/lib/groups/checkGroupMemberships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function checkGroupMemberships(em: EntityManager, player: P

if (playerIsEligible && !playerCurrentlyInGroup) {
group.members.add(player)
} else if (playerCurrentlyInGroup) {
} else if (!playerIsEligible && playerCurrentlyInGroup) {
group.members.remove(group.members.getItems().find((member) => member.id === player.id))
}
}
Expand Down
116 changes: 116 additions & 0 deletions src/migrations/.snapshot-gs_dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -3122,6 +3122,122 @@
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "int",
"unsigned": true,
"autoincrement": true,
"primary": true,
"nullable": false,
"length": null,
"mappedType": "integer"
},
"user_id": {
"name": "user_id",
"type": "int",
"unsigned": true,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": null,
"mappedType": "integer"
},
"group_id": {
"name": "group_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 255,
"mappedType": "string"
},
"created_at": {
"name": "created_at",
"type": "datetime",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": null,
"mappedType": "datetime"
}
},
"name": "user_pinned_group",
"indexes": [
{
"columnNames": [
"user_id"
],
"composite": false,
"keyName": "user_pinned_group_user_id_index",
"constraint": false,
"primary": false,
"unique": false
},
{
"columnNames": [
"group_id"
],
"composite": false,
"keyName": "user_pinned_group_group_id_index",
"constraint": false,
"primary": false,
"unique": false
},
{
"keyName": "user_pinned_group_user_id_group_id_unique",
"columnNames": [
"user_id",
"group_id"
],
"composite": true,
"constraint": true,
"primary": false,
"unique": true
},
{
"keyName": "PRIMARY",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"user_pinned_group_user_id_foreign": {
"constraintName": "user_pinned_group_user_id_foreign",
"columnNames": [
"user_id"
],
"localTableName": "user_pinned_group",
"referencedColumnNames": [
"id"
],
"referencedTableName": "user",
"updateRule": "cascade"
},
"user_pinned_group_group_id_foreign": {
"constraintName": "user_pinned_group_group_id_foreign",
"columnNames": [
"group_id"
],
"localTableName": "user_pinned_group",
"referencedColumnNames": [
"id"
],
"referencedTableName": "player_group",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
Expand Down
19 changes: 19 additions & 0 deletions src/migrations/20241001194252CreateUserPinnedGroupsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Migration } from '@mikro-orm/migrations'

export class CreateUserPinnedGroupsTable extends Migration {

override async up(): Promise<void> {
this.addSql('create table `user_pinned_group` (`id` int unsigned not null auto_increment primary key, `user_id` int unsigned not null, `group_id` varchar(255) not null, `created_at` datetime not null) default character set utf8mb4 engine = InnoDB;')
this.addSql('alter table `user_pinned_group` add index `user_pinned_group_user_id_index`(`user_id`);')
this.addSql('alter table `user_pinned_group` add index `user_pinned_group_group_id_index`(`group_id`);')
this.addSql('alter table `user_pinned_group` add unique `user_pinned_group_user_id_group_id_unique`(`user_id`, `group_id`);')

this.addSql('alter table `user_pinned_group` add constraint `user_pinned_group_user_id_foreign` foreign key (`user_id`) references `user` (`id`) on update cascade;')
this.addSql('alter table `user_pinned_group` add constraint `user_pinned_group_group_id_foreign` foreign key (`group_id`) references `player_group` (`id`) on update cascade;')
}

override async down(): Promise<void> {
this.addSql('drop table if exists `user_pinned_group`;')
}

}
5 changes: 5 additions & 0 deletions src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { CreatePlayerAuthActivityTable } from './20240725183402CreatePlayerAuthA
import { UpdatePlayerAliasServiceColumn } from './20240916213402UpdatePlayerAliasServiceColumn'
import { AddPlayerAliasAnonymisedColumn } from './20240920121232AddPlayerAliasAnonymisedColumn'
import { AddLeaderboardEntryPropsColumn } from './20240922222426AddLeaderboardEntryPropsColumn'
import { CreateUserPinnedGroupsTable } from './20241001194252CreateUserPinnedGroupsTable'

export default [
{
Expand Down Expand Up @@ -154,5 +155,9 @@ export default [
{
name: 'AddLeaderboardEntryPropsColumn',
class: AddLeaderboardEntryPropsColumn
},
{
name: 'CreateUserPinnedGroupsTable',
class: CreateUserPinnedGroupsTable
}
]
14 changes: 14 additions & 0 deletions src/policies/player-group.policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,18 @@ export default class PlayerGroupPolicy extends Policy {

return await this.canAccessGame(Number(gameId))
}

async indexPinned(req: Request): Promise<PolicyResponse> {
const { gameId } = req.params
return await this.canAccessGame(Number(gameId))
}

async togglePinned(req: Request): Promise<PolicyResponse> {
const { gameId, id } = req.params

this.ctx.state.group = await this.em.getRepository(PlayerGroup).findOne(id)
if (!this.ctx.state.group) return new PolicyDenial({ message: 'Group not found' }, 404)

return await this.canAccessGame(Number(gameId))
}
}
51 changes: 51 additions & 0 deletions src/services/player-group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import PlayerGroupRule, { PlayerGroupRuleCastType, PlayerGroupRuleName } from '.
import { ruleModeValidation, rulesValidation } from '../lib/groups/rulesValidation'
import createGameActivity from '../lib/logging/createGameActivity'
import PlayerGroupPolicy from '../policies/player-group.policy'
import getUserFromToken from '../lib/auth/getUserFromToken'
import UserPinnedGroup from '../entities/user-pinned-group'

type PlayerGroupWithCount = Pick<PlayerGroup, 'id' | 'name' | 'description' | 'rules' | 'ruleMode' | 'updatedAt'> & { count: number }

Expand All @@ -31,6 +33,16 @@ type PlayerGroupWithCount = Pick<PlayerGroup, 'id' | 'name' | 'description' | 'r
method: 'GET',
path: '/preview-count',
handler: 'previewCount'
},
{
method: 'GET',
path: '/pinned',
handler: 'indexPinned'
},
{
method: 'PUT',
path: '/:id/toggle-pinned',
handler: 'togglePinned'
}
])
export default class PlayerGroupService extends Service {
Expand Down Expand Up @@ -195,6 +207,7 @@ export default class PlayerGroupService extends Service {
}
})

await em.getRepository(UserPinnedGroup).nativeDelete({ group: req.ctx.state.group })
await em.removeAndFlush(req.ctx.state.group)

return {
Expand Down Expand Up @@ -232,4 +245,42 @@ export default class PlayerGroupService extends Service {
}
}
}

@HasPermission(PlayerGroupPolicy, 'indexPinned')
async indexPinned(req: Request): Promise<Response> {
const em: EntityManager = req.ctx.em
const user = await getUserFromToken(req.ctx)

const pinnedGroups = await em.getRepository(UserPinnedGroup).find({ user }, { orderBy: { createdAt: 'desc' } })
const groups = await Promise.all(pinnedGroups.map(({ group }) => this.groupWithCount(group)))

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

@HasPermission(PlayerGroupPolicy, 'togglePinned')
async togglePinned(req: Request): Promise<Response> {
const { pinned } = req.body
const em: EntityManager = req.ctx.em

const group: PlayerGroup = req.ctx.state.group
const user = await getUserFromToken(req.ctx)

const pinnedGroup = await em.getRepository(UserPinnedGroup).findOne({ user, group })
if (pinned && !pinnedGroup) {
em.persist(new UserPinnedGroup(user, group))
} else if (!pinned && pinnedGroup) {
em.remove(pinnedGroup)
}

await em.flush()

return {
status: 204
}
}
}
3 changes: 3 additions & 0 deletions src/services/player.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ export default class PlayerService extends Service {

await em.flush()

// refresh groups
await em.refresh(player)

return {
status: 200,
body: {
Expand Down
2 changes: 1 addition & 1 deletion src/services/public/user-public.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default class UserPublicService extends Service {

createGameActivity(em, { user, type: GameActivityType.INVITE_ACCEPTED })

await em.remove(invite)
em.remove(invite)
} else {
const organisation = new Organisation()
organisation.email = email
Expand Down
2 changes: 1 addition & 1 deletion src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default class UserService extends Service {
user.password = await bcrypt.hash(newPassword, 10)
const userSessionRepo = em.getRepository(UserSession)
const sessions = await userSessionRepo.find({ user })
await em.remove(sessions)
em.remove(sessions)

const accessToken = await buildTokenPair(req.ctx, user)

Expand Down
24 changes: 24 additions & 0 deletions tests/fixtures/UserPinnedGroupFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Factory } from 'hefty'
import UserPinnedGroup from '../../src/entities/user-pinned-group'
import UserFactory from './UserFactory'
import PlayerGroupFactory from './PlayerGroupFactory'
import GameFactory from './GameFactory'
import OrganisationFactory from './OrganisationFactory'

export default class UserPinnedGroupFactory extends Factory<UserPinnedGroup> {
constructor() {
super(UserPinnedGroup)
}

protected definition(): void {
this.state(async () => {
const organisation = await new OrganisationFactory().one()
const game = await new GameFactory(organisation).one()

return {
user: await new UserFactory().state(() => ({ organisation })).one(),
group: await new PlayerGroupFactory().state(() => ({ game })).one()
}
})
}
}
Loading

0 comments on commit 4cc93cc

Please sign in to comment.