From f614712a3a760b8752a800053997aa7b973fc3f1 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Sun, 11 Sep 2022 19:52:16 +0100 Subject: [PATCH] make player props their own separate entity --- src/entities/event.ts | 2 +- src/entities/index.ts | 2 + src/entities/player-prop.ts | 23 +++++ src/entities/player.ts | 20 +++-- .../integrations/steamworks-integration.ts | 3 +- src/middlewares/dev-data-middleware.ts | 12 ++- src/migrations/.snapshot-gs_dev.json | 85 +++++++++++++++++-- .../20220910200720CreatePlayerPropsTable.ts | 22 +++++ src/migrations/index.ts | 5 ++ src/services/api/player-api.service.ts | 5 +- src/services/data-export.service.ts | 34 +++++--- src/services/event.service.ts | 5 +- src/services/headline.service.ts | 17 ++-- src/services/leaderboard.service.ts | 2 +- src/services/player.service.ts | 22 +++-- tests/fixtures/LeaderboardFactory.ts | 3 +- tests/fixtures/PlayerFactory.ts | 10 +-- tests/services/_api/event-api/post.test.ts | 9 +- tests/services/_api/player-api/merge.test.ts | 55 ++++-------- tests/services/_api/player-api/patch.test.ts | 30 +++---- tests/services/data-export/generation.test.ts | 13 +-- tests/services/player/index.test.ts | 14 ++- tests/services/player/patch.test.ts | 39 ++++----- 23 files changed, 271 insertions(+), 161 deletions(-) create mode 100644 src/entities/player-prop.ts create mode 100644 src/migrations/20220910200720CreatePlayerPropsTable.ts diff --git a/src/entities/event.ts b/src/entities/event.ts index ddc1b4af..18fbf2b3 100644 --- a/src/entities/event.ts +++ b/src/entities/event.ts @@ -41,7 +41,7 @@ export default class Event { this.props.forEach((prop) => { if (eventMetaProps.includes(prop.key)) { - this.playerAlias.player.props.push(prop) + this.playerAlias.player.addProp(prop.key, prop.value) } }) } diff --git a/src/entities/index.ts b/src/entities/index.ts index c742fb1d..a731f1c4 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -26,8 +26,10 @@ import OrganisationPricingPlanAction from './organisation-pricing-plan-action' import Integration from './integration' import SteamworksIntegrationEvent from './steamworks-integration-event' import SteamworksLeaderboardMapping from './steamworks-leaderboard-mapping' +import PlayerProp from './player-prop' export default [ + PlayerProp, SteamworksLeaderboardMapping, SteamworksIntegrationEvent, Integration, diff --git a/src/entities/player-prop.ts b/src/entities/player-prop.ts new file mode 100644 index 00000000..98f0245a --- /dev/null +++ b/src/entities/player-prop.ts @@ -0,0 +1,23 @@ +import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' +import Player from './player' + +@Entity() +export default class PlayerProp { + @PrimaryKey() + id: number + + @ManyToOne(() => Player) + player: Player + + @Property() + key: string + + @Property() + value: string + + constructor(player: Player, key: string, value: string) { + this.player = player + this.key = key + this.value = value + } +} diff --git a/src/entities/player.ts b/src/entities/player.ts index 8db5a870..84cc08cb 100644 --- a/src/entities/player.ts +++ b/src/entities/player.ts @@ -1,8 +1,8 @@ -import { Collection, Embedded, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' +import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import Game from './game' import { v4 } from 'uuid' import PlayerAlias from './player-alias' -import Prop from './prop' +import PlayerProp from './player-prop' @Entity() export default class Player { @@ -12,8 +12,8 @@ export default class Player { @OneToMany(() => PlayerAlias, (alias) => alias.player) aliases: Collection = new Collection(this) - @Embedded(() => Prop, { array: true }) - props: Prop[] = [] + @OneToMany(() => PlayerProp, (prop) => prop.player, { eager: true, orphanRemoval: true }) + props: Collection = new Collection(this) @ManyToOne(() => Game) game: Game @@ -32,13 +32,21 @@ export default class Player { } isDevBuild() { - return this.props.some((prop) => prop.key === 'META_DEV_BUILD') + return this.props.getItems().some((prop) => prop.key === 'META_DEV_BUILD') + } + + addProp(key: string, value: string) { + this.props.add(new PlayerProp(this, key, value)) + } + + setProps(props: { key: string, value: string }[]) { + this.props.set(props.map(({ key, value }) => new PlayerProp(this, key, value))) } toJSON() { return { id: this.id, - props: this.props, + props: this.props.getItems().map(({ key, value }) => ({ key, value })), aliases: this.aliases, devBuild: this.isDevBuild(), createdAt: this.createdAt, diff --git a/src/lib/integrations/steamworks-integration.ts b/src/lib/integrations/steamworks-integration.ts index 997e8136..6339f6d3 100644 --- a/src/lib/integrations/steamworks-integration.ts +++ b/src/lib/integrations/steamworks-integration.ts @@ -8,7 +8,6 @@ import LeaderboardEntry from '../../entities/leaderboard-entry' import SteamworksLeaderboardMapping from '../../entities/steamworks-leaderboard-mapping' import PlayerAlias, { PlayerAliasService } from '../../entities/player-alias' import Player from '../../entities/player' -import Prop from '../../entities/prop' import { performance } from 'perf_hooks' import GameStat from '../../entities/game-stat' import PlayerGameStat from '../../entities/player-game-stat' @@ -338,7 +337,7 @@ export async function syncSteamworksLeaderboards(em: EntityManager, integration: // if the alias doesnt exist then neither does the entry, so create both } else { const player = new Player(integration.game) - player.props.push(new Prop('importedFromSteam', new Date().toISOString())) + player.addProp('importedFromSteam', new Date().toISOString()) const playerAlias = new PlayerAlias() playerAlias.player = player diff --git a/src/middlewares/dev-data-middleware.ts b/src/middlewares/dev-data-middleware.ts index 25985450..e5e39eba 100644 --- a/src/middlewares/dev-data-middleware.ts +++ b/src/middlewares/dev-data-middleware.ts @@ -1,6 +1,8 @@ -import { expr, ObjectQuery } from '@mikro-orm/core' +import { QBFilterQuery } from '@mikro-orm/core' +import { EntityManager } from '@mikro-orm/mysql' import { Context, Next } from 'koa' import Player from '../entities/player' +import PlayerProp from '../entities/player-prop' export default async (ctx: Context, next: Next): Promise => { if (Number(ctx.headers['x-talo-include-dev-data'])) { @@ -10,6 +12,10 @@ export default async (ctx: Context, next: Next): Promise => { await next() } -export const devDataPlayerFilter: ObjectQuery = { - [expr((alias) => `json_search(${alias}.props, 'one', 'META_DEV_BUILD', null, '$[*].key')`)]: null +export function devDataPlayerFilter(em: EntityManager): QBFilterQuery { + return { + $nin: em.qb(PlayerProp).select('player_id', true).where({ + key: 'META_DEV_BUILD' + }).getKnexQuery() + } } diff --git a/src/migrations/.snapshot-gs_dev.json b/src/migrations/.snapshot-gs_dev.json index 8a7eb376..469c5cc3 100644 --- a/src/migrations/.snapshot-gs_dev.json +++ b/src/migrations/.snapshot-gs_dev.json @@ -878,15 +878,6 @@ "nullable": false, "mappedType": "string" }, - "props": { - "name": "props", - "type": "json", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "json" - }, "game_id": { "name": "game_id", "type": "int", @@ -2823,6 +2814,82 @@ "deleteRule": "cascade" } } + }, + { + "columns": { + "id": { + "name": "id", + "type": "int", + "unsigned": true, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "player_id": { + "name": "player_id", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "string" + }, + "key": { + "name": "key", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "string" + }, + "value": { + "name": "value", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "string" + } + }, + "name": "player_prop", + "indexes": [ + { + "columnNames": [ + "player_id" + ], + "composite": false, + "keyName": "player_prop_player_id_index", + "primary": false, + "unique": false + }, + { + "keyName": "PRIMARY", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "player_prop_player_id_foreign": { + "constraintName": "player_prop_player_id_foreign", + "columnNames": [ + "player_id" + ], + "localTableName": "player_prop", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "player", + "updateRule": "cascade" + } + } } ] } diff --git a/src/migrations/20220910200720CreatePlayerPropsTable.ts b/src/migrations/20220910200720CreatePlayerPropsTable.ts new file mode 100644 index 00000000..6b9e45e6 --- /dev/null +++ b/src/migrations/20220910200720CreatePlayerPropsTable.ts @@ -0,0 +1,22 @@ +import { Migration } from '@mikro-orm/migrations' + +export class CreatePlayerPropsTable extends Migration { + + async up(): Promise { + this.addSql('create table `player_prop` (`id` int unsigned not null auto_increment primary key, `player_id` varchar(255) not null, `key` varchar(255) not null, `value` varchar(255) not null) default character set utf8mb4 engine = InnoDB;') + this.addSql('alter table `player_prop` add index `player_prop_player_id_index`(`player_id`);') + + this.addSql('alter table `player_prop` add constraint `player_prop_player_id_foreign` foreign key (`player_id`) references `player` (`id`) on update cascade;') + + this.addSql('insert into `player_prop` (`player_id`, `key`, `value`) select `p`.`id`, `r`.* from `player` as `p`, json_table(`p`.`props`, \'$[*]\' columns (`key` varchar(255) path \'$.key\', `value` varchar(255) path \'$.value\')) as `r`') + + this.addSql('alter table `player` drop `props`;') + } + + async down(): Promise { + this.addSql('drop table if exists `player_prop`;') + + this.addSql('alter table `player` add `props` json not null;') + } + +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index f6c6bd2d..18f92a81 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -16,6 +16,7 @@ import { CreatePricingPlansTable } from './20220603123117CreatePricingPlansTable import { CreateIntegrationsTable } from './20220717215205CreateIntegrationsTable' import { CreateSteamIntegrationTables } from './20220723122554CreateSteamIntegrationTables' import { PlayerAliasServiceUseEnum } from './20220730134520PlayerAliasServiceUseEnum' +import { CreatePlayerPropsTable } from './20220910200720CreatePlayerPropsTable' export default [ { @@ -89,5 +90,9 @@ export default [ { name: 'PlayerAliasServiceUseEnum', class: PlayerAliasServiceUseEnum + }, + { + name: 'CreatePlayerPropsTable', + class: CreatePlayerPropsTable } ] diff --git a/src/services/api/player-api.service.ts b/src/services/api/player-api.service.ts index c833133a..34b86231 100644 --- a/src/services/api/player-api.service.ts +++ b/src/services/api/player-api.service.ts @@ -7,6 +7,7 @@ import PlayerAPIPolicy from '../../policies/api/player-api.policy' import APIService from './api-service' import uniqWith from 'lodash.uniqwith' import PlayerAPIDocs from '../../docs/player-api.docs' +import PlayerProp from '../../entities/player-prop' @Routes([ { @@ -126,12 +127,12 @@ export default class PlayerAPIService extends APIService { player2Aliases.forEach((alias) => alias.player = player1) - const mergedProps = uniqWith([ + const mergedProps: PlayerProp[] = uniqWith([ ...player2.props, ...player1.props ], (a, b) => a.key === b.key) - player1.props = Array.from(mergedProps) + player1.props.set(Array.from(mergedProps)) await em.removeAndFlush(player2) diff --git a/src/services/data-export.service.ts b/src/services/data-export.service.ts index b0df029d..57bb04ca 100644 --- a/src/services/data-export.service.ts +++ b/src/services/data-export.service.ts @@ -1,4 +1,4 @@ -import { EntityManager, FilterQuery, MikroORM, ObjectQuery } from '@mikro-orm/core' +import { Collection, FilterQuery, MikroORM } from '@mikro-orm/core' import { HasPermission, Routes, Service, Request, Response, Validate, ValidationCondition } from 'koa-clay' import DataExport, { DataExportAvailableEntities, DataExportStatus } from '../entities/data-export' import DataExportPolicy from '../policies/data-export.policy' @@ -23,9 +23,14 @@ import handlePricingPlanAction from '../lib/billing/handlePricingPlanAction' import { PricingPlanActionType } from '../entities/pricing-plan-action' import queueEmail from '../lib/messaging/queueEmail' import OrganisationPricingPlanAction from '../entities/organisation-pricing-plan-action' +import { EntityManager } from '@mikro-orm/mysql' +import pick from 'lodash.pick' +import PlayerProp from '../entities/player-prop' + +type PropCollection = Collection interface EntityWithProps { - props: Prop[] + props: Prop[] | PropCollection } interface UpdatedDataExportStatus { @@ -76,7 +81,7 @@ export default class DataExportService extends Service { const filename = `export-${dataExport.game.id}-${dataExport.createdAt.getTime()}.zip` const filepath = './storage/' + filename - const zip: AdmZip = await this.createZip(dataExport, orm.em, includeDevData) + const zip: AdmZip = await this.createZip(dataExport, orm.em as EntityManager, includeDevData) zip.writeZip(filepath) dataExport.status = DataExportStatus.QUEUED @@ -136,7 +141,7 @@ export default class DataExportService extends Service { if (!includeDevData) { where.playerAlias = { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } } @@ -147,7 +152,7 @@ export default class DataExportService extends Service { let where: FilterQuery = { game: dataExport.game } if (!includeDevData) { - where = { ...where, ...devDataPlayerFilter } + where = Object.assign(where, devDataPlayerFilter(em)) } return await em.getRepository(Player).find(where) @@ -159,10 +164,7 @@ export default class DataExportService extends Service { } if (!includeDevData) { - where.player = { - ...(where.player as ObjectQuery), - ...devDataPlayerFilter - } + where.player = Object.assign(where.player, devDataPlayerFilter(em)) } return await em.getRepository(PlayerAlias).find(where) @@ -175,7 +177,7 @@ export default class DataExportService extends Service { if (!includeDevData) { where.playerAlias = { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } } @@ -205,7 +207,7 @@ export default class DataExportService extends Service { } if (!includeDevData) { - where.player = devDataPlayerFilter + where.player = devDataPlayerFilter(em) } return await em.getRepository(PlayerGameStat).find(where) @@ -279,9 +281,15 @@ export default class DataExportService extends Service { } } + private getProps(object: ExportableEntityWithProps): { key: string, value: string }[] { + let props = object.props + if (props instanceof Collection) props = props.getItems() + return props.map((prop) => pick(prop, ['key', 'value'])) + } + private transformColumn(column: string, object: ExportableEntity): string { if (column.startsWith('props')) { - const value = (object as ExportableEntityWithProps).props.find((prop) => column.endsWith(prop.key))?.value ?? '' + const value = this.getProps(object as ExportableEntityWithProps).find((prop) => column.endsWith(prop.key))?.value ?? '' return value } @@ -309,7 +317,7 @@ export default class DataExportService extends Service { columns = columns.filter((col) => col !== 'props') const allProps = objects.reduce((acc: string[], curr: EntityWithProps): string[] => { - return [...acc, ...curr.props.map((prop) => `props.${prop.key}`)] + return [...acc, ...this.getProps(curr as ExportableEntityWithProps).map((prop) => `props.${prop.key}`)] }, []).sort((a, b) => a.localeCompare(b)) columns = [...new Set([...columns, ...allProps])] diff --git a/src/services/event.service.ts b/src/services/event.service.ts index 6e45ac33..0150e0fd 100644 --- a/src/services/event.service.ts +++ b/src/services/event.service.ts @@ -1,4 +1,4 @@ -import { EntityManager, FilterQuery } from '@mikro-orm/core' +import { FilterQuery } from '@mikro-orm/core' import { HasPermission, Service, Request, Response, Validate } from 'koa-clay' import Event from '../entities/event' import EventPolicy from '../policies/event.policy' @@ -6,6 +6,7 @@ import groupBy from 'lodash.groupby' import { isSameDay, endOfDay } from 'date-fns' import dateValidationSchema from '../lib/dates/dateValidationSchema' import { devDataPlayerFilter } from '../middlewares/dev-data-middleware' +import { EntityManager } from '@mikro-orm/mysql' interface EventData { name: string @@ -33,7 +34,7 @@ export default class EventService extends Service { if (!req.ctx.state.includeDevData) { where.playerAlias = { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } } diff --git a/src/services/headline.service.ts b/src/services/headline.service.ts index e81153d2..812aaf5c 100644 --- a/src/services/headline.service.ts +++ b/src/services/headline.service.ts @@ -1,4 +1,4 @@ -import { EntityManager, FilterQuery } from '@mikro-orm/core' +import { FilterQuery } from '@mikro-orm/core' import { endOfDay, isSameDay } from 'date-fns' import { Service, Request, Response, Validate, HasPermission, Routes } from 'koa-clay' import groupBy from 'lodash.groupby' @@ -7,6 +7,7 @@ import Player from '../entities/player' import HeadlinePolicy from '../policies/headline.policy' import dateValidationSchema from '../lib/dates/dateValidationSchema' import { devDataPlayerFilter } from '../middlewares/dev-data-middleware' +import { EntityManager } from '@mikro-orm/mysql' @Routes([ { @@ -46,10 +47,7 @@ export default class HeadlineService extends Service { } if (!req.ctx.state.includeDevData) { - where = { - ...where, - ...devDataPlayerFilter - } + where = Object.assign(where, devDataPlayerFilter(em)) } const players = await em.getRepository(Player).find(where) @@ -80,10 +78,7 @@ export default class HeadlineService extends Service { } if (!req.ctx.state.includeDevData) { - where = { - ...where, - ...devDataPlayerFilter - } + where = Object.assign(where, devDataPlayerFilter(em)) } let players = await em.getRepository(Player).find(where) @@ -113,7 +108,7 @@ export default class HeadlineService extends Service { if (!req.ctx.state.includeDevData) { where.playerAlias = { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } } @@ -143,7 +138,7 @@ export default class HeadlineService extends Service { if (!req.ctx.state.includeDevData) { where.playerAlias = { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } } diff --git a/src/services/leaderboard.service.ts b/src/services/leaderboard.service.ts index c71dc29f..b0db7a55 100644 --- a/src/services/leaderboard.service.ts +++ b/src/services/leaderboard.service.ts @@ -126,7 +126,7 @@ export default class LeaderboardService extends Service { if (!req.ctx.state.includeDevData) { baseQuery = baseQuery.andWhere({ playerAlias: { - player: devDataPlayerFilter + player: devDataPlayerFilter(em) } }) } diff --git a/src/services/player.service.ts b/src/services/player.service.ts index e11ba74c..e794afb2 100644 --- a/src/services/player.service.ts +++ b/src/services/player.service.ts @@ -12,7 +12,7 @@ import createGameActivity from '../lib/logging/createGameActivity' import { GameActivityType } from '../entities/game-activity' import PlayerGameStat from '../entities/player-game-stat' import { devDataPlayerFilter } from '../middlewares/dev-data-middleware' -import Prop from '../entities/prop' +import PlayerProp from '../entities/player-prop' const itemsPerPage = 25 @@ -71,11 +71,11 @@ export default class PlayerService extends Service { } if (props) { - player.props = sanitiseProps(props) + player.setProps(props) } if (req.headers['x-talo-dev-build'] === '1') { - player.props.push(new Prop('META_DEV_BUILD', '1')) + player.addProp('META_DEV_BUILD', '1') } await em.persistAndFlush(player) @@ -93,11 +93,19 @@ export default class PlayerService extends Service { const { search, page } = req.query const em: EntityManager = req.ctx.em - let baseQuery = em.createQueryBuilder(Player, 'p') + let baseQuery = em.qb(Player, 'p') if (search) { baseQuery = baseQuery - .where('json_extract(props, \'$[*].value\') like ?', [`%${search}%`]) + .where({ + props: { + $in: em.qb(PlayerProp).select('id').where({ + value: { + $like: `%${search}%` + } + }).getKnexQuery() + } + }) .orWhere({ aliases: { identifier: { @@ -113,7 +121,7 @@ export default class PlayerService extends Service { } if (!req.ctx.state.includeDevData) { - baseQuery = baseQuery.andWhere(devDataPlayerFilter) + baseQuery = baseQuery.andWhere(devDataPlayerFilter(em)) } baseQuery = baseQuery.andWhere({ game: req.ctx.state.game }) @@ -164,7 +172,7 @@ export default class PlayerService extends Service { ...player.props ], (a, b) => a.key === b.key) - player.props = sanitiseProps(mergedProps, true) + player.setProps(sanitiseProps(mergedProps, true)) } if (req.ctx.state.user.api !== true) { diff --git a/tests/fixtures/LeaderboardFactory.ts b/tests/fixtures/LeaderboardFactory.ts index d2d58891..155ff184 100644 --- a/tests/fixtures/LeaderboardFactory.ts +++ b/tests/fixtures/LeaderboardFactory.ts @@ -5,7 +5,6 @@ import Game from '../../src/entities/game' import LeaderboardEntryFactory from './LeaderboardEntryFactory' import { Collection } from '@mikro-orm/core' import LeaderboardEntry from '../../src/entities/leaderboard-entry' -import Prop from '../../src/entities/prop' export default class LeaderboardFactory extends Factory { private availableGames: Game[] @@ -56,7 +55,7 @@ export default class LeaderboardFactory extends Factory { protected async devBuildPlayers(leaderboard: Leaderboard): Promise> { leaderboard.entries.getItems().forEach((entry) => { - entry.playerAlias.player.props.push(new Prop('META_DEV_BUILD', '1')) + entry.playerAlias.player.addProp('META_DEV_BUILD', '1') }) return leaderboard diff --git a/tests/fixtures/PlayerFactory.ts b/tests/fixtures/PlayerFactory.ts index bcde0c0c..764bd11b 100644 --- a/tests/fixtures/PlayerFactory.ts +++ b/tests/fixtures/PlayerFactory.ts @@ -6,7 +6,7 @@ import PlayerAliasFactory from './PlayerAliasFactory' import { Collection } from '@mikro-orm/core' import PlayerAlias from '../../src/entities/player-alias' import { sub } from 'date-fns' -import Prop from '../../src/entities/prop' +import PlayerProp from '../../src/entities/player-prop' export default class PlayerFactory extends Factory { private availableGames: Game[] @@ -29,10 +29,10 @@ export default class PlayerFactory extends Factory { protected async base(player: Player): Promise> { const availableProps = ['zonesExplored', 'currentArea', 'position.x', 'position.y', 'deaths', 'position.z', 'currentLevel', 'inventorySpace', 'currentHealth', 'currentMana', 'currentEnergy', 'npcKills', 'playerKills'] const propsCount = casual.integer(0, 5) - const props: Prop[] = [] + const props: PlayerProp[] = [] for (let i = 0; i < propsCount; i++) { - props.push(new Prop(casual.random_element(availableProps), String(casual.integer(0, 99)))) + props.push(new PlayerProp(player, casual.random_element(availableProps), String(casual.integer(0, 99)))) } const playerAliasFactory = new PlayerAliasFactory() @@ -44,7 +44,7 @@ export default class PlayerFactory extends Factory { return { aliases, game: casual.random_element(this.availableGames), - props, + props: new Collection(player, props), lastSeenAt } } @@ -81,7 +81,7 @@ export default class PlayerFactory extends Factory { } protected devBuild(player: Player): Partial { - player.props.push(new Prop('META_DEV_BUILD', '1')) + player.addProp('META_DEV_BUILD', '1') return player } diff --git a/tests/services/_api/event-api/post.test.ts b/tests/services/_api/event-api/post.test.ts index 05df3c08..d7fbba0b 100644 --- a/tests/services/_api/event-api/post.test.ts +++ b/tests/services/_api/event-api/post.test.ts @@ -9,6 +9,7 @@ import { createToken } from '../../../../src/services/api-key.service' import UserFactory from '../../../fixtures/UserFactory' import PlayerFactory from '../../../fixtures/PlayerFactory' import GameFactory from '../../../fixtures/GameFactory' +import PlayerProp from '../../../../src/entities/player-prop' const baseUrl = '/v1/events' @@ -244,8 +245,12 @@ describe('Event API service - post', () => { .auth(token, { type: 'bearer' }) .expect(200) - const player = await (app.context.em).getRepository(Player).findOne(validPlayer.id, { refresh: true }) - expect(player.props).toContainEqual({ key: 'META_OS', value: 'macOS' }) + const prop = await (app.context.em).getRepository(PlayerProp).findOne({ + player: validPlayer.id, + key: 'META_OS', + value: 'macOS' + }) + expect(prop).toBeTruthy() }) it('should strip out event props that start with META_ but aren\'t in the meta props list', async () => { diff --git a/tests/services/_api/player-api/merge.test.ts b/tests/services/_api/player-api/merge.test.ts index 6e19fdd2..8afaf576 100644 --- a/tests/services/_api/player-api/merge.test.ts +++ b/tests/services/_api/player-api/merge.test.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/core' +import { Collection, EntityManager } from '@mikro-orm/core' import Koa from 'koa' import init from '../../../../src/index' import request from 'supertest' @@ -10,6 +10,7 @@ import GameFactory from '../../../fixtures/GameFactory' import PlayerFactory from '../../../fixtures/PlayerFactory' import Player from '../../../../src/entities/player' import PlayerAlias from '../../../../src/entities/player-alias' +import PlayerProp from '../../../../src/entities/player-prop' const baseUrl = '/v1/players/merge' @@ -108,46 +109,22 @@ describe('Player API service - merge', () => { apiKey.scopes = [APIKeyScope.READ_PLAYERS, APIKeyScope.WRITE_PLAYERS] token = await createToken(apiKey) - const player1 = await new PlayerFactory([apiKey.game]).with(() => ({ - props: [ - { - key: 'currentLevel', - value: '60' - }, - { - key: 'currentHealth', - value: '66' - }, - { - key: 'pos.x', - value: '50' - }, - { - key: 'pos.y', - value: '-30' - } - ] + const player1 = await new PlayerFactory([apiKey.game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'currentLevel', '60'), + new PlayerProp(player, 'currentHealth', '66'), + new PlayerProp(player, 'pos.x', '50'), + new PlayerProp(player, 'pos.y', '-30') + ]) })).one() - const player2 = await new PlayerFactory([apiKey.game]).with(() => ({ - props: [ - { - key: 'currentLevel', - value: '60' - }, - { - key: 'pos.x', - value: '58' - }, - { - key: 'pos.y', - value: '-24' - }, - { - key: 'pos.z', - value: '4' - } - ] + const player2 = await new PlayerFactory([apiKey.game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'currentLevel', '60'), + new PlayerProp(player, 'pos.x', '58'), + new PlayerProp(player, 'pos.y', '-24'), + new PlayerProp(player, 'pos.z', '4') + ]) })).one() await (app.context.em).persistAndFlush([player1, player2]) diff --git a/tests/services/_api/player-api/patch.test.ts b/tests/services/_api/player-api/patch.test.ts index f83d27dd..40174777 100644 --- a/tests/services/_api/player-api/patch.test.ts +++ b/tests/services/_api/player-api/patch.test.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/core' +import { Collection, EntityManager } from '@mikro-orm/core' import Koa from 'koa' import init from '../../../../src/index' import request from 'supertest' @@ -7,8 +7,8 @@ import APIKey, { APIKeyScope } from '../../../../src/entities/api-key' import { createToken } from '../../../../src/services/api-key.service' import UserFactory from '../../../fixtures/UserFactory' import PlayerFactory from '../../../fixtures/PlayerFactory' -import Prop from '../../../../src/entities/prop' import createOrganisationAndGame from '../../../utils/createOrganisationAndGame' +import PlayerProp from '../../../../src/entities/player-prop' const baseUrl = '/v1/players' @@ -32,11 +32,11 @@ describe('Player API service - patch', () => { }) it('should update a player\'s properties', async () => { - const player = await new PlayerFactory([apiKey.game]).with(() => ({ - props: [ - new Prop('collectibles', '0'), - new Prop('zonesExplored', '1') - ] + const player = await new PlayerFactory([apiKey.game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'collectibles', '0'), + new PlayerProp(player, 'zonesExplored', '1') + ]) })).one() await (app.context.em).persistAndFlush(player) @@ -70,17 +70,11 @@ describe('Player API service - patch', () => { }) it('should not update a player\'s properties if the scope is missing', async () => { - const player = await new PlayerFactory([apiKey.game]).with(() => ({ - props: [ - { - key: 'collectibles', - value: '0' - }, - { - key: 'zonesExplored', - value: '1' - } - ] + const player = await new PlayerFactory([apiKey.game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'collectibles', '0'), + new PlayerProp(player, 'zonesExplored', '1') + ]) })).one() await (app.context.em).persistAndFlush(player) diff --git a/tests/services/data-export/generation.test.ts b/tests/services/data-export/generation.test.ts index dc79e3e1..2ba42187 100644 --- a/tests/services/data-export/generation.test.ts +++ b/tests/services/data-export/generation.test.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/core' +import { Collection, EntityManager } from '@mikro-orm/core' import Koa from 'koa' import init from '../../../src/index' import User from '../../../src/entities/user' @@ -18,6 +18,7 @@ import OrganisationPricingPlanActionFactory from '../../fixtures/OrganisationPri import { PricingPlanActionType } from '../../../src/entities/pricing-plan-action' import OrganisationPricingPlanAction from '../../../src/entities/organisation-pricing-plan-action' import PricingPlanFactory from '../../fixtures/PricingPlanFactory' +import PlayerProp from '../../../src/entities/player-prop' describe('Data export service - generation', () => { let app: Koa @@ -60,11 +61,11 @@ describe('Data export service - generation', () => { const service = new DataExportService() const proto = Object.getPrototypeOf(service) - const player = await new PlayerFactory([game]).with(() => ({ - props: [ - { key: 'level', value: '70' }, - { key: 'guildName', value: 'The Best Guild' } - ] + const player = await new PlayerFactory([game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'level', '70'), + new PlayerProp(player, 'guildName', 'The Best Guild') + ]) })).one() let val: string = proto.transformColumn('props.level', player) diff --git a/tests/services/player/index.test.ts b/tests/services/player/index.test.ts index 2ac986b5..dfda44bd 100644 --- a/tests/services/player/index.test.ts +++ b/tests/services/player/index.test.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/core' +import { Collection, EntityManager } from '@mikro-orm/core' import Koa from 'koa' import init from '../../../src/index' import request from 'supertest' @@ -9,6 +9,7 @@ import UserFactory from '../../fixtures/UserFactory' import OrganisationFactory from '../../fixtures/OrganisationFactory' import PlayerFactory from '../../fixtures/PlayerFactory' import Player from '../../../src/entities/player' +import PlayerProp from '../../../src/entities/player-prop' describe('Player service - index', () => { let app: Koa @@ -68,13 +69,10 @@ describe('Player service - index', () => { }) it('should filter players by props', async () => { - const players = await new PlayerFactory([validGame]).with(() => ({ - props: [ - { - key: 'guildName', - value: 'The Best Guild' - } - ] + const players = await new PlayerFactory([validGame]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'guildName', 'The Best Guild') + ]) })).many(2) const otherPlayers = await new PlayerFactory([validGame]).many(3) diff --git a/tests/services/player/patch.test.ts b/tests/services/player/patch.test.ts index b0d94dfc..d84e5165 100644 --- a/tests/services/player/patch.test.ts +++ b/tests/services/player/patch.test.ts @@ -1,4 +1,4 @@ -import { EntityManager } from '@mikro-orm/core' +import { Collection, EntityManager } from '@mikro-orm/core' import Koa from 'koa' import init from '../../../src/index' import request from 'supertest' @@ -8,6 +8,7 @@ import GameActivity, { GameActivityType } from '../../../src/entities/game-activ import userPermissionProvider from '../../utils/userPermissionProvider' import createOrganisationAndGame from '../../utils/createOrganisationAndGame' import createUserAndToken from '../../utils/createUserAndToken' +import PlayerProp from '../../../src/entities/player-prop' describe('Player service - patch', () => { let app: Koa @@ -27,17 +28,11 @@ describe('Player service - patch', () => { const [organisation, game] = await createOrganisationAndGame(app.context.em) const [token] = await createUserAndToken(app.context.em, { type }, organisation) - const player = await new PlayerFactory([game]).with(() => ({ - props: [ - { - key: 'collectibles', - value: '0' - }, - { - key: 'zonesExplored', - value: '1' - } - ] + const player = await new PlayerFactory([game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'collectibles', '0'), + new PlayerProp(player, 'zonesExplored', '1') + ]) })).one() await (app.context.em).persistAndFlush(player) @@ -86,17 +81,11 @@ describe('Player service - patch', () => { const [organisation, game] = await createOrganisationAndGame(app.context.em) const [token] = await createUserAndToken(app.context.em, {}, organisation) - const player = await new PlayerFactory([game]).with(() => ({ - props: [ - { - key: 'collectibles', - value: '1' - }, - { - key: 'zonesExplored', - value: '1' - } - ] + const player = await new PlayerFactory([game]).with((player) => ({ + props: new Collection(player, [ + new PlayerProp(player, 'collectibles', '1'), + new PlayerProp(player, 'zonesExplored', '1') + ]) })).one() await (app.context.em).persistAndFlush(player) @@ -197,7 +186,9 @@ describe('Player service - patch', () => { const [organisation, game] = await createOrganisationAndGame(app.context.em) const [token] = await createUserAndToken(app.context.em, {}, organisation) - const player = await new PlayerFactory([game]).with(() => ({ props: [] })).one() + const player = await new PlayerFactory([game]).with((player) => ({ + props: new Collection(player, []) + })).one() await (app.context.em).persistAndFlush(player) const res = await request(app.callback())