Skip to content

Commit

Permalink
Merge pull request #332 from TaloDev/develop
Browse files Browse the repository at this point in the history
Release 0.41.0
  • Loading branch information
tudddorrr authored Sep 8, 2024
2 parents 3f70730 + 47fe447 commit 195b4c5
Show file tree
Hide file tree
Showing 14 changed files with 181 additions and 34 deletions.
2 changes: 1 addition & 1 deletion clickhouse/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM clickhouse/clickhouse-server:24.6-alpine
FROM clickhouse/clickhouse-server:24.8-alpine

RUN apk add --no-cache gettext

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
test-redis:
image: bitnami/redis:7.2
image: bitnami/redis:7.4
command:
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "game-services",
"version": "0.40.0",
"version": "0.41.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"watch": "tsx watch src/index.ts",
"build": "npx tsc -p tsconfig.build.json",
"dc": "docker compose -f docker-compose.yml -f docker-compose.dev.yml",
"seed": "DB_HOST=127.0.0.1 tsx tests/seed.ts",
"seed": "DB_HOST=127.0.0.1 CLICKHOUSE_HOST=127.0.0.1 tsx tests/seed.ts",
"test": "./tests/run-tests.sh",
"up": "npm run dc -- up --build -d",
"down": "npm run dc -- down",
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 @@ -16,7 +16,8 @@ export enum DataExportAvailableEntities {
LEADERBOARD_ENTRIES = 'leaderboardEntries',
GAME_STATS = 'gameStats',
PLAYER_GAME_STATS = 'playerGameStats',
GAME_ACTIVITIES = 'gameActivities'
GAME_ACTIVITIES = 'gameActivities',
GAME_FEEDBACK = 'gameFeedback'
}

@Entity()
Expand Down
4 changes: 2 additions & 2 deletions src/entities/game-stat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default class GameStat {

@Required({
validation: async (value: number, req: Request): Promise<ValidationCondition[]> => [{
check: value < (req.body.maxValue ?? Infinity),
check: req.body.maxValue ? value < req.body.maxValue : true,
error: 'minValue must be less than maxValue'
}]
})
Expand All @@ -55,7 +55,7 @@ export default class GameStat {

@Required({
validation: async (value: number, req: Request): Promise<ValidationCondition[]> => [{
check: value > (req.body.minValue ?? -Infinity),
check: req.body.minValue ? value > req.body.minValue : true,
error: 'maxValue must be greater than minValue'
}]
})
Expand Down
43 changes: 40 additions & 3 deletions src/services/data-export.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Job, Queue } from 'bullmq'
import createEmailQueue from '../lib/queues/createEmailQueue'
import { EmailConfig } from '../lib/messaging/sendEmail'
import createClickhouseClient from '../lib/clickhouse/createClient'
import GameFeedback from '../entities/game-feedback'

type PropCollection = Collection<PlayerProp, Player>

Expand All @@ -45,7 +46,7 @@ type DataExportJob = {
includeDevData: boolean
}

type ExportableEntity = Event | Player | PlayerAlias | LeaderboardEntry | GameStat | PlayerGameStat | GameActivity
type ExportableEntity = Event | Player | PlayerAlias | LeaderboardEntry | GameStat | PlayerGameStat | GameActivity | GameFeedback
type ExportableEntityWithProps = ExportableEntity & EntityWithProps

@Routes([
Expand Down Expand Up @@ -221,7 +222,7 @@ export default class DataExportService extends Service {
where.player = devDataPlayerFilter(em)
}

return await em.getRepository(PlayerGameStat).find(where)
return await em.getRepository(PlayerGameStat).find(where, { populate: ['player'] })
}

private async getGameActivities(dataExport: DataExport, em: EntityManager): Promise<GameActivity[]> {
Expand All @@ -232,6 +233,26 @@ export default class DataExportService extends Service {
})
}

private async getGameFeedback(dataExport: DataExport, em: EntityManager, includeDevData: boolean): Promise<GameFeedback[]> {
const where: FilterQuery<GameFeedback> = {
playerAlias: {
player: {
game: dataExport.game
}
}
}

if (!includeDevData) {
where.playerAlias = Object.assign(where.playerAlias, {
player: devDataPlayerFilter(em)
})
}

return await em.getRepository(GameFeedback).find(where, {
populate: ['playerAlias.player']
})
}

private async createZip(dataExport: DataExport, em: EntityManager, includeDevData: boolean): Promise<AdmZip> {
const zip = new AdmZip()

Expand Down Expand Up @@ -270,6 +291,11 @@ export default class DataExportService extends Service {
zip.addFile(`${DataExportAvailableEntities.GAME_ACTIVITIES}.csv`, this.buildCSV(DataExportAvailableEntities.GAME_ACTIVITIES, items))
}

if (dataExport.entities.includes(DataExportAvailableEntities.GAME_FEEDBACK)) {
const items = await this.getGameFeedback(dataExport, em, includeDevData)
zip.addFile(`${DataExportAvailableEntities.GAME_FEEDBACK}.csv`, this.buildCSV(DataExportAvailableEntities.GAME_FEEDBACK, items))
}

return zip
}

Expand All @@ -286,9 +312,11 @@ export default class DataExportService extends Service {
case DataExportAvailableEntities.GAME_STATS:
return ['id', 'internalName', 'name', 'defaultValue', 'minValue', 'maxValue', 'global', 'globalValue', 'createdAt', 'updatedAt']
case DataExportAvailableEntities.PLAYER_GAME_STATS:
return ['id', 'value', 'stat.id', 'stat.internalName', 'createdAt', 'updatedAt']
return ['id', 'player.id', 'value', 'stat.id', 'stat.internalName', 'createdAt', 'updatedAt']
case DataExportAvailableEntities.GAME_ACTIVITIES:
return ['id', 'user.username', 'gameActivityType', 'gameActivityExtra', 'createdAt']
case DataExportAvailableEntities.GAME_FEEDBACK:
return ['id', 'category.internalName', 'comment', 'playerAlias.id', 'playerAlias.service', 'playerAlias.identifier', 'playerAlias.player.id', 'createdAt']
}
}

Expand Down Expand Up @@ -319,6 +347,15 @@ export default class DataExportService extends Service {
return `"${JSON.stringify(value).replace(/"/g, '\'')}"`
case 'globalValue':
return get(object, 'hydratedGlobalValue') ?? 'N/A'
case 'playerAlias.id':
case 'playerAlias.service':
case 'playerAlias.identifier':
case 'playerAlias.player.id':
if (object instanceof GameFeedback && object.anonymised) {
return 'Anonymous'
} else {
return String(value)
}
default:
return String(value)
}
Expand Down
11 changes: 8 additions & 3 deletions src/services/player.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ export default class PlayerService extends Service {
const player = new Player(game)
if (aliases) {
for await (const alias of aliases) {
if (await em.getRepository(PlayerAlias).count({ service: alias.service, identifier: alias.identifier }) > 0) {
const count = await em.getRepository(PlayerAlias).count({
service: alias.service,
identifier: alias.identifier,
player: { game }
})

if (count > 0) {
req.ctx.throw(400, {
message: `Player with identifier ${alias.identifier} already exists`,
errorCode: PlayerAuthErrorCode.IDENTIFIER_TAKEN
Expand Down Expand Up @@ -120,7 +126,6 @@ export default class PlayerService extends Service {
}
}

@Validate({ query: ['page'] })
@HasPermission(PlayerPolicy, 'index')
async index(req: Request): Promise<Response> {
const itemsPerPage = 25
Expand All @@ -132,7 +137,7 @@ export default class PlayerService extends Service {
.select('p.*')
.orderBy({ lastSeenAt: QueryOrder.DESC })
.limit(itemsPerPage)
.offset(Number(page) * itemsPerPage)
.offset(Number(page ?? 0) * itemsPerPage)

if (search) {
query
Expand Down
25 changes: 12 additions & 13 deletions tests/lib/integrations/steamworksSyncStats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Steamworks integration - sync stats', () => {
it('should pull in stats from steamworks', async () => {
const [, game] = await createOrganisationAndGame(em)

const statDisplayName = casual.word
const statDisplayName = casual.words(3)

const config = await new IntegrationConfigFactory().one()
const integration = await new IntegrationFactory().construct(IntegrationType.STEAMWORKS, game, config).one()
Expand All @@ -44,7 +44,7 @@ describe('Steamworks integration - sync stats', () => {
availableGameStats: {
stats: [
{
name: 'stat_' + casual.word,
name: 'stat_' + casual.array_of_words(3).join('-'),
defaultvalue: 500,
displayName: statDisplayName
}
Expand Down Expand Up @@ -81,8 +81,8 @@ describe('Steamworks integration - sync stats', () => {
it('should update existing stats with the name and default value from steamworks', async () => {
const [, game] = await createOrganisationAndGame(em)

const statName = 'stat_' + casual.word
const statDisplayName = casual.word
const statName = 'stat_' + casual.array_of_words(3).join('-')
const statDisplayName = casual.words(3)

const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one()

Expand Down Expand Up @@ -121,7 +121,7 @@ describe('Steamworks integration - sync stats', () => {
it('should pull in player stats from steamworks', async () => {
const [, game] = await createOrganisationAndGame(em)

const statName = 'stat_' + casual.word
const statName = 'stat_' + casual.array_of_words(3).join('-')

const player = await new PlayerFactory([game]).withSteamAlias().one()

Expand All @@ -139,7 +139,7 @@ describe('Steamworks integration - sync stats', () => {
{
name: statName,
defaultvalue: 500,
displayName: casual.word
displayName: casual.words(3)
}
],
achievements: []
Expand Down Expand Up @@ -187,9 +187,9 @@ describe('Steamworks integration - sync stats', () => {
availableGameStats: {
stats: [
{
name: 'stat_' + casual.word,
name: 'stat_' + casual.array_of_words(3).join('-'),
defaultvalue: 500,
displayName: casual.word
displayName: casual.words(3)
}
],
achievements: []
Expand All @@ -213,7 +213,7 @@ describe('Steamworks integration - sync stats', () => {
it('should update player stats with the ones from steamworks', async () => {
const [, game] = await createOrganisationAndGame(em)

const statName = 'stat_' + casual.word
const statName = 'stat_' + casual.array_of_words(3).join('-')

const player = await new PlayerFactory([game]).withSteamAlias().one()
const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one()
Expand All @@ -233,7 +233,7 @@ describe('Steamworks integration - sync stats', () => {
{
name: statName,
defaultvalue: 500,
displayName: casual.word
displayName: casual.words(3)
}
],
achievements: []
Expand All @@ -260,14 +260,13 @@ describe('Steamworks integration - sync stats', () => {
expect(getSchemaMock).toHaveBeenCalledTimes(1)
expect(getUserStatsMock).toHaveBeenCalledTimes(1)

await em.refresh(playerStat)
expect(playerStat.value).toBe(301)
})

it('should push through player stats that only exist in talo', async () => {
const [, game] = await createOrganisationAndGame(em)

const statName = 'stat_' + casual.word
const statName = 'stat_' + casual.array_of_words(3).join('-')

const player = await new PlayerFactory([game]).withSteamAlias().one()
const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one()
Expand All @@ -287,7 +286,7 @@ describe('Steamworks integration - sync stats', () => {
{
name: statName,
defaultvalue: 500,
displayName: casual.word
displayName: casual.words(3)
}
],
achievements: []
Expand Down
40 changes: 35 additions & 5 deletions tests/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import PricingPlanAction, { PricingPlanActionType } from '../src/entities/pricin
import OrganisationPricingPlanFactory from './fixtures/OrganisationPricingPlanFactory'
import PricingPlan from '../src/entities/pricing-plan'
import APIKey, { APIKeyScope } from '../src/entities/api-key'
import GameFeedbackCategoryFactory from './fixtures/GameFeedbackCategoryFactory'
import GameFeedbackFactory from './fixtures/GameFeedbackFactory'
import createClickhouseClient from '../src/lib/clickhouse/createClient'

(async () => {
console.info('Running migrations...')
Expand All @@ -25,7 +28,7 @@ import APIKey, { APIKeyScope } from '../src/entities/api-key'
await orm.em.getConnection().execute('drop table if exists mikro_orm_migrations')
await orm.getMigrator().up()

console.info('Seeding database...')
console.info('Seeding DB...')

const plansMap: Partial<PricingPlan>[] = [
{ stripeId: 'prod_LcO5U04wEGWgMP', default: true },
Expand Down Expand Up @@ -97,8 +100,6 @@ import APIKey, { APIKeyScope } from '../src/entities/api-key'

const players = await new PlayerFactory(games).many(50)

const eventsThisMonth = await new EventFactory(players).thisMonth().many(300)

const leaderboards = await new LeaderboardFactory(games).withEntries().many(6)

const gameSaves = await new GameSaveFactory(players).many(10)
Expand All @@ -114,6 +115,23 @@ import APIKey, { APIKeyScope } from '../src/entities/api-key'
}
}

const feedback = []
for (const game of games) {
const categories = [
await new GameFeedbackCategoryFactory(game).state(() => ({ internalName: 'bugs', name: 'Bugs', anonymised: false })).one(),
await new GameFeedbackCategoryFactory(game).state(() => ({ internalName: 'feedback', name: 'Feedback', anonymised: true })).one()
]

for (const category of categories) {
const items = await new GameFeedbackFactory(game).state(() => ({
category,
playerAlias: casual.random_element(players.filter((player) => player.game === game)).aliases[0],
anonymised: category.anonymised
})).many(5)
feedback.push(...items)
}
}

const em = orm.em.fork()

await em.persistAndFlush([
Expand All @@ -126,14 +144,26 @@ import APIKey, { APIKeyScope } from '../src/entities/api-key'
...games,
...apiKeys,
...players,
...eventsThisMonth,
...leaderboards,
...gameSaves,
...gameStats,
...playerGameStats
...playerGameStats,
...feedback
])

await orm.close(true)

const eventsThisMonth = await new EventFactory(players).thisMonth().many(300)

console.info('Seeding Clickhouse...')

const clickhouse = createClickhouseClient()
await clickhouse.insert({
table: 'events',
values: eventsThisMonth.map((event) => event.getInsertableData()),
format: 'JSONEachRow'
})
clickhouse.close()

console.info('Done!')
})()
2 changes: 1 addition & 1 deletion tests/services/data-export/entities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ describe('Data export service - available entities', () => {
.auth(token, { type: 'bearer' })
.expect(200)

expect(res.body.entities).toStrictEqual([ 'events', 'players', 'playerAliases', 'leaderboardEntries', 'gameStats', 'playerGameStats', 'gameActivities' ])
expect(res.body.entities).toStrictEqual([ 'events', 'players', 'playerAliases', 'leaderboardEntries', 'gameStats', 'playerGameStats', 'gameActivities', 'gameFeedback' ])
})
})
Loading

0 comments on commit 195b4c5

Please sign in to comment.