Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {MigrationInterface, QueryRunner} from "typeorm";

/**
* mledb.player_account had a unique constraint on tracker. Multiple platform rows
* often omit or share tracker values during Sprocket-primary mirroring; uniqueness
* on tracker is not required for identity (platform + platform_id is authoritative).
*/
export class DropMledbPlayerAccountTrackerUnique1773000000000 implements MigrationInterface {
name = "DropMledbPlayerAccountTrackerUnique1773000000000";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'DROP INDEX IF EXISTS "mledb"."player_account_tracker_unique"',
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE UNIQUE INDEX "player_account_tracker_unique" ON "mledb"."player_account" ("tracker")',
);
}
}
1 change: 0 additions & 1 deletion core/src/database/mledb/PlayerAccount.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {MLE_Player} from "./Player.model";

@Index("player_account_pkey", ["id"], {unique: true})
@Index("player_account_platform_id_platform_unique", ["platform", "platformId"], {unique: true})
@Index("player_account_tracker_unique", ["tracker"], {unique: true})
@Entity("player_account", {schema: "mledb"})
export class MLE_PlayerAccount {
@PrimaryGeneratedColumn({type: "integer", name: "id"})
Expand Down
14 changes: 14 additions & 0 deletions core/src/franchise/player/player.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {Organization} from "../../database/organization/organization/organizatio
import {EloConnectorService} from "../../elo/elo-connector";
import {PlatformService} from "../../game";
import {OrganizationService} from "../../organization";
import {MemberPlatformAccountService} from "../../organization/member-platform-account";
import {MemberService} from "../../organization/member/member.service";
import {MledbPlayerAccountService} from "../../mledb";
import {GameSkillGroupService} from "../game-skill-group";
import {PlayerService} from "./player.service";
import {OperationError} from "./player.types";
Expand Down Expand Up @@ -224,6 +226,18 @@ describe("PlayerService", () => {
send: jest.fn(),
},
},
{
provide: MemberPlatformAccountService,
useValue: {
upsertMemberPlatformAccount: jest.fn(),
},
},
{
provide: MledbPlayerAccountService,
useValue: {
createOrUpdatePlayerAccount: jest.fn(),
},
},
],
}).compile();

Expand Down
76 changes: 75 additions & 1 deletion core/src/franchise/player/player.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import {UserAuthenticationAccount} from "../../database/identity/user_authentica
import {UserAuthenticationAccountType} from "../../database/identity/user_authentication_account/user_authentication_account_type.enum";
import {UserProfile} from "../../database/identity/user_profile/user_profile.model";
import {
League, LeagueOrdinals, ModePreference, Role, Timezone,
League, LeagueOrdinals, ModePreference, MLE_Platform, Role, Timezone,
} from "../../database/mledb";
import {MLE_Player} from "../../database/mledb/Player.model";
import {PlayerToPlayer} from "../../database/mledb-bridge/player_to_player.model";
import {GameSkillGroup} from "../../database/franchise/game_skill_group/game_skill_group.model";
import {Platform} from "../../database/game/platform/platform.model";
import {Member} from "../../database/organization/member/member.model";
import {MemberProfile} from "../../database/organization/member_profile/member_profile.model";
import {Organization} from "../../database/organization/organization/organization.model";
Expand All @@ -45,6 +47,8 @@ import {
SkillGroupDelta,
} from "../../elo/elo-connector";
import {PlatformService} from "../../game";
import {MledbPlayerAccountService} from "../../mledb";
import {MemberPlatformAccountService} from "../../organization/member-platform-account";
import {OrganizationService} from "../../organization";
import {MemberService} from "../../organization/member/member.service";
import {GameSkillGroupService} from "../game-skill-group";
Expand Down Expand Up @@ -83,6 +87,10 @@ export class PlayerService {
private readonly eloConnectorService: EloConnectorService,
private readonly platformService: PlatformService,
private readonly analyticsService: AnalyticsService,
@Inject(forwardRef(() => MemberPlatformAccountService))
private readonly memberPlatformAccountService: MemberPlatformAccountService,
@Inject(forwardRef(() => MledbPlayerAccountService))
private readonly mledbPlayerAccountService: MledbPlayerAccountService,
) {}

async getPlayer(query: FindOneOptions<Player>): Promise<Player> {
Expand Down Expand Up @@ -277,6 +285,64 @@ export class PlayerService {
);
}

/**
* Persist Rocket League Steam IDs on the Sprocket member; mirror to mledb.player_account when an MLE_Player bridge exists (cutover).
*/
private async linkRocketLeagueSteamIdsForPlayer(
member: Member,
player: Player,
steamIds: string[] | undefined,
runner: QueryRunner,
updatedByUserId: number,
): Promise<void> {
const uniq = [...new Set((steamIds ?? []).map(id => id.trim()).filter(id => id.length > 0))];
if (uniq.length === 0) return;

const skillGroup = await this.skillGroupService.getGameSkillGroup({
where: {id: player.skillGroupId},
relations: {game: true},
});
if (skillGroup.game.id !== 7) {
this.logger.warn(
`Steam account IDs supplied for non-Rocket-League skill group ${skillGroup.id}; skipping platform link`,
);
return;
}

let steamPlatform: Platform;
try {
steamPlatform = await this.platformService.getPlatformByCode(MLE_Platform.STEAM, runner.manager);
} catch {
steamPlatform = await this.platformService.createPlatform(MLE_Platform.STEAM);
}

const bridge = await runner.manager.findOne(PlayerToPlayer, {
where: {sprocketPlayerId: player.id},
});
const mlePlayer = bridge
? await runner.manager.findOne(MLE_Player, {where: {id: bridge.mledPlayerId} })
: null;

for (const steamId of uniq) {
await this.memberPlatformAccountService.upsertMemberPlatformAccount(
member,
steamPlatform.id,
steamId,
runner.manager,
);
if (mlePlayer) {
await this.mledbPlayerAccountService.createOrUpdatePlayerAccount(
updatedByUserId,
MLE_Platform.STEAM,
steamId,
steamId,
mlePlayer,
runner.manager,
);
}
}
}

/* !! Using repositories due to circular dependency issues. Will fix after extended repositories are added, probably. !! */
async updatePlayer(
mleid: number,
Expand Down Expand Up @@ -1240,6 +1306,14 @@ export class PlayerService {
const player = await this.createPlayer(member.id, pt.gameSkillGroupId, pt.salary, runner);
this.logger.log(`Created player: id=${player.id}, skillGroupId=${pt.gameSkillGroupId}, salary=${pt.salary}`);

await this.linkRocketLeagueSteamIdsForPlayer(
member,
player,
pt.accountSteamIds,
runner,
member.userId,
);

const skillGroup = await this.skillGroupService.getGameSkillGroupById(
pt.gameSkillGroupId,
{relations: {profile: true} },
Expand Down
4 changes: 4 additions & 0 deletions core/src/franchise/player/player.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export class CreatePlayerTuple {

@Field(() => Float)
salary: number;

/** Steam platform IDs for Rocket League; optional. Written to Sprocket and mirrored to mledb.player_account during cutover. */
@Field(() => [String], {nullable: true})
accountSteamIds?: string[];
}

export const IntakeSchema = z
Expand Down
71 changes: 35 additions & 36 deletions core/src/identity/auth/oauth/strategies/discord.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ export class DiscordStrategy extends PassportStrategy(Strategy, "discord") {
profile: Profile,
done: Done,
): Promise<User | undefined> {
const mledbPlayer = await this.mledbPlayerService
.getPlayerByDiscordId(profile.id)
.catch(() => null);
if (!mledbPlayer) throw new Error("User is not associated with MLE");

const userByDiscordId = await this.identityService
.getUserByAuthAccount(UserAuthenticationAccountType.DISCORD, profile.id)
.catch(() => undefined);
Expand All @@ -75,6 +70,12 @@ export class DiscordStrategy extends PassportStrategy(Strategy, "discord") {
// TODO: Do we want to actually do this? Theoretically, if a user changes their email, that's a "new user" if we go by email. Hence ^
if (!user) user = await this.userService.getUser({where: {email: profile.email} });

// New-account import still keys off legacy MLEDB until full cutover; returning users authenticate via Sprocket alone.
let mledbPlayer = await this.mledbPlayerService
.getPlayerByDiscordId(profile.id)
.catch(() => null);
if (!user && !mledbPlayer) throw new Error("User is not associated with MLE");

// If no users returned from query, create a new one
if (!user) {
const userProfile: Omit<UserProfile, IrrelevantFields | "id" | "user"> = {
Expand Down Expand Up @@ -113,63 +114,61 @@ export class DiscordStrategy extends PassportStrategy(Strategy, "discord") {
await this.userService.addAuthenticationAccounts(user.id, [authAcct]);
}

if (!mledbPlayer) {
mledbPlayer = await this.mledbPlayerService
.getPlayerByDiscordId(profile.id)
.catch(() => null);
}

let member = await this.memberService
.getMember({where: {user: {id: user.id} } })
.catch(() => null);

if (!member) {
const displayName = mledbPlayer?.name ?? profile.username;
member = await this.memberService.createMember(
{name: mledbPlayer.name},
{name: displayName},
MLE_ORGANIZATION_ID,
user.id,
);
}

const mledbPlayerAccounts = await this.mledbPlayerAccountService.getPlayerAccounts({
where: {player: {id: mledbPlayer.id} },
});
if (mledbPlayer) {
const mledbPlayerAccounts = await this.mledbPlayerAccountService.getPlayerAccounts({
where: {player: {id: mledbPlayer.id} },
});

for (const mledbPlayerAccount of mledbPlayerAccounts) {
if (!mledbPlayerAccount.platformId) continue;
for (const mledbPlayerAccount of mledbPlayerAccounts) {
if (!mledbPlayerAccount.platformId) continue;

const platformAccount = await this.memberPlatformAccountService
.getMemberPlatformAccount({
where: {
member: {id: member.id},
platform: {code: mledbPlayerAccount.platform},
platformAccountId: mledbPlayerAccount.platformId,
},
relations: ["member", "platform"],
})
.catch(() => null);

if (!platformAccount) {
const platform = await this.platformService
.getPlatformByCode(mledbPlayerAccount.platform)
.catch(async () => this.platformService.createPlatform(mledbPlayerAccount.platform));

await this.memberPlatformAccountService.createMemberPlatformAccount(
await this.memberPlatformAccountService.upsertMemberPlatformAccount(
member,
platform.id,
mledbPlayerAccount.platformId,
);
}
}

if (!["PREMIER", "MASTER", "CHAMPION", "ACADEMY", "FOUNDATION"].includes(mledbPlayer.league)) throw new Error("Player does not belong to a league");
if (mledbPlayer) {
if (!["PREMIER", "MASTER", "CHAMPION", "ACADEMY", "FOUNDATION"].includes(mledbPlayer.league)) throw new Error("Player does not belong to a league");

const skillGroup = await this.skillGroupService.getGameSkillGroup({
where: {
profile: {
code: `${mledbPlayer.league[0]}L`,
const skillGroup = await this.skillGroupService.getGameSkillGroup({
where: {
profile: {
code: `${mledbPlayer.league[0]}L`,
},
},
},
relations: ["profile"],
});
const player = await this.playerService
.getPlayer({where: {member: {id: member.id} } })
.catch(() => null);
if (!player) await this.playerService.createPlayer(member, skillGroup.id, mledbPlayer.salary);
relations: ["profile"],
});
const player = await this.playerService
.getPlayer({where: {member: {id: member.id} } })
.catch(() => null);
if (!player) await this.playerService.createPlayer(member, skillGroup.id, mledbPlayer.salary);
}

done("", user);
return user;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import {EntityManager, Repository} from "typeorm";
import type {MLE_Platform, MLE_Player} from "../../database/mledb";
import {MLE_PlayerAccount} from "../../database/mledb";

/**
* Legacy mirror of `sprocket.member_platform_account` into `mledb.player_account` for cutover.
* Fields written: platform, platform_id, tracker (often same as platform_id for Steam), updated_by, player_id.
* Removal plan: stop calling `createOrUpdatePlayerAccount` from write paths; then drop this service and
* `mledb.player_account` writes once reporting and imports read only from Sprocket.
*/
@Injectable()
export class MledbPlayerAccountService {
constructor(@InjectRepository(MLE_PlayerAccount)
Expand Down
Loading
Loading