338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { PrismaService } from '../../shared/prisma/prisma.service';
|
|
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
|
|
|
|
export interface AgentScope {
|
|
rootAgentId: bigint;
|
|
rootLevel: number;
|
|
subtreeIds: bigint[];
|
|
descendantIds: bigint[];
|
|
directChildIds: bigint[];
|
|
subtreeIdSet: Set<string>;
|
|
}
|
|
|
|
type ClosureRow = {
|
|
ancestorId?: bigint;
|
|
descendantId: bigint;
|
|
depth: number;
|
|
};
|
|
|
|
function uniqueBigints(values: bigint[]) {
|
|
const seen = new Set<string>();
|
|
const out: bigint[] = [];
|
|
for (const value of values) {
|
|
const key = value.toString();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
out.push(value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
@Injectable()
|
|
export class AgentNetworkService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async buildAgentAncestorChainMap(parentAgentIds: (bigint | null | undefined)[]) {
|
|
return this.buildAncestorChainMap(parentAgentIds, undefined);
|
|
}
|
|
|
|
async buildScopedAncestorChainMap(
|
|
parentAgentIds: (bigint | null | undefined)[],
|
|
rootAgentId: bigint,
|
|
) {
|
|
return this.buildAncestorChainMap(parentAgentIds, rootAgentId);
|
|
}
|
|
|
|
async resolveScope(rootAgentId: bigint): Promise<AgentScope> {
|
|
const [profile, closureRows] = await Promise.all([
|
|
this.prisma.agentProfile.findUnique({
|
|
where: { userId: rootAgentId },
|
|
}),
|
|
this.findSubtreeClosureRows(rootAgentId),
|
|
]);
|
|
|
|
if (!profile) throw appBadRequest('AGENT_PROFILE_NOT_FOUND');
|
|
|
|
const hasClosureRows = closureRows.length > 0;
|
|
const subtreeIds = hasClosureRows
|
|
? this.idsFromSubtreeClosure(rootAgentId, closureRows)
|
|
: await this.getSubtreeAgentIdsFromProfiles(rootAgentId);
|
|
const descendantIds = subtreeIds.filter((id) => id !== rootAgentId);
|
|
const directChildIds = hasClosureRows
|
|
? uniqueBigints(
|
|
closureRows
|
|
.filter((row) => row.depth === 1)
|
|
.map((row) => row.descendantId),
|
|
)
|
|
: await this.getDirectChildAgentIds(rootAgentId);
|
|
|
|
return {
|
|
rootAgentId,
|
|
rootLevel: profile.level,
|
|
subtreeIds,
|
|
descendantIds,
|
|
directChildIds,
|
|
subtreeIdSet: new Set(subtreeIds.map((id) => id.toString())),
|
|
};
|
|
}
|
|
|
|
assertAgentInScope(scope: AgentScope, agentId: bigint) {
|
|
if (!scope.subtreeIdSet.has(agentId.toString())) {
|
|
throw appForbidden('NOT_SUB_AGENT');
|
|
}
|
|
}
|
|
|
|
async requirePlayerInPortalSubtree(rootAgentId: bigint, playerId: bigint) {
|
|
const scope = await this.resolveScope(rootAgentId);
|
|
const player = await this.prisma.user.findFirst({
|
|
where: { id: playerId, userType: 'PLAYER', deletedAt: null },
|
|
include: { wallet: true, preferences: true, auth: true },
|
|
});
|
|
if (!player?.parentId || !scope.subtreeIdSet.has(player.parentId.toString())) {
|
|
throw appForbidden('MANAGE_DIRECT_PLAYERS_ONLY');
|
|
}
|
|
return { player, scope, isDirect: player.parentId.toString() === rootAgentId.toString() };
|
|
}
|
|
|
|
async getChildAgents(agentId: bigint) {
|
|
return this.prisma.agentProfile.findMany({
|
|
where: { parentAgentId: agentId },
|
|
include: { user: true },
|
|
});
|
|
}
|
|
|
|
async assertDescendantAgent(rootAgentId: bigint, targetAgentId: bigint) {
|
|
const subtreeIds = await this.getSubtreeAgentIds(rootAgentId);
|
|
if (!subtreeIds.some((id) => id === targetAgentId)) {
|
|
throw appForbidden('NOT_SUB_AGENT');
|
|
}
|
|
const profile = await this.prisma.agentProfile.findUnique({
|
|
where: { userId: targetAgentId },
|
|
});
|
|
if (!profile) throw appNotFound('AGENT_NOT_FOUND');
|
|
return profile;
|
|
}
|
|
|
|
async assertDirectChildAgent(parentAgentId: bigint, subAgentId: bigint) {
|
|
const profile = await this.prisma.agentProfile.findUnique({
|
|
where: { userId: subAgentId },
|
|
});
|
|
if (!profile || profile.parentAgentId !== parentAgentId) {
|
|
throw appForbidden('NOT_SUB_AGENT');
|
|
}
|
|
return profile;
|
|
}
|
|
|
|
async getSubtreeAgentIds(agentId: bigint) {
|
|
const closureRows = await this.findSubtreeClosureRows(agentId);
|
|
if (closureRows.length > 0) {
|
|
return this.idsFromSubtreeClosure(agentId, closureRows);
|
|
}
|
|
return this.getSubtreeAgentIdsFromProfiles(agentId);
|
|
}
|
|
|
|
private async buildAncestorChainMap(
|
|
parentAgentIds: (bigint | null | undefined)[],
|
|
scopedRootAgentId: bigint | undefined,
|
|
) {
|
|
const agentIds = uniqueBigints(parentAgentIds.filter((id): id is bigint => id != null));
|
|
const map = new Map<string, string[]>();
|
|
if (agentIds.length === 0) return map;
|
|
|
|
const closureRows = await this.prisma.agentClosure.findMany({
|
|
where: { descendantId: { in: agentIds } },
|
|
select: { ancestorId: true, descendantId: true, depth: true },
|
|
orderBy: [{ descendantId: 'asc' }, { depth: 'desc' }],
|
|
});
|
|
const rowsByDescendant = new Map<string, ClosureRow[]>();
|
|
for (const row of closureRows) {
|
|
const key = row.descendantId.toString();
|
|
rowsByDescendant.set(key, [...(rowsByDescendant.get(key) ?? []), row]);
|
|
}
|
|
|
|
const idsNeedingProfileFallback: bigint[] = [];
|
|
const ancestorIds = new Set<bigint>();
|
|
for (const id of agentIds) {
|
|
const rows = rowsByDescendant.get(id.toString());
|
|
if (!rows || rows.length === 0) {
|
|
idsNeedingProfileFallback.push(id);
|
|
continue;
|
|
}
|
|
const scopedRows = scopedRootAgentId
|
|
? this.sliceRowsToScopedRoot(rows, scopedRootAgentId)
|
|
: rows;
|
|
for (const row of scopedRows) {
|
|
if (row.ancestorId) ancestorIds.add(row.ancestorId);
|
|
}
|
|
}
|
|
|
|
const users =
|
|
ancestorIds.size > 0
|
|
? await this.prisma.user.findMany({
|
|
where: { id: { in: [...ancestorIds] } },
|
|
select: { id: true, username: true },
|
|
})
|
|
: [];
|
|
const usernameMap = new Map(users.map((u) => [u.id.toString(), u.username]));
|
|
|
|
for (const id of agentIds) {
|
|
const rows = rowsByDescendant.get(id.toString());
|
|
if (!rows || rows.length === 0) continue;
|
|
|
|
const scopedRows = scopedRootAgentId
|
|
? this.sliceRowsToScopedRoot(rows, scopedRootAgentId)
|
|
: rows;
|
|
const chain = scopedRows
|
|
.map((row) => (row.ancestorId ? usernameMap.get(row.ancestorId.toString()) : undefined))
|
|
.filter((username): username is string => Boolean(username));
|
|
map.set(id.toString(), chain.length === scopedRows.length ? chain : []);
|
|
}
|
|
|
|
if (idsNeedingProfileFallback.length > 0) {
|
|
const fallbackMap = scopedRootAgentId
|
|
? await this.buildScopedAncestorChainMapFromProfiles(idsNeedingProfileFallback, scopedRootAgentId)
|
|
: await this.buildAncestorChainMapFromProfiles(idsNeedingProfileFallback);
|
|
for (const [key, value] of fallbackMap) {
|
|
map.set(key, value);
|
|
}
|
|
}
|
|
|
|
for (const id of parentAgentIds) {
|
|
if (id && !map.has(id.toString())) {
|
|
map.set(id.toString(), []);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private sliceRowsToScopedRoot(rows: ClosureRow[], rootAgentId: bigint) {
|
|
const rootRow = rows.find((row) => row.ancestorId === rootAgentId);
|
|
if (!rootRow) return [];
|
|
return rows.filter((row) => row.depth <= rootRow.depth);
|
|
}
|
|
|
|
private async buildAncestorChainMapFromProfiles(agentIds: bigint[]) {
|
|
const cache = await this.loadAncestorProfileCache(agentIds, undefined);
|
|
|
|
const build = (startId: bigint | null | undefined): string[] => {
|
|
const chain: string[] = [];
|
|
let cur = startId ?? null;
|
|
while (cur) {
|
|
const hit = cache.get(cur.toString());
|
|
if (!hit) break;
|
|
chain.unshift(hit.username);
|
|
cur = hit.parentAgentId;
|
|
}
|
|
return chain;
|
|
};
|
|
|
|
return new Map(agentIds.map((id) => [id.toString(), build(id)]));
|
|
}
|
|
|
|
private async buildScopedAncestorChainMapFromProfiles(agentIds: bigint[], rootAgentId: bigint) {
|
|
const rootKey = rootAgentId.toString();
|
|
const cache = await this.loadAncestorProfileCache(agentIds, rootAgentId);
|
|
|
|
const build = (startId: bigint | null | undefined): string[] => {
|
|
if (!startId) return [];
|
|
const chain: string[] = [];
|
|
let cur: bigint | null = startId;
|
|
let reachedRoot = false;
|
|
while (cur) {
|
|
const hit = cache.get(cur.toString());
|
|
if (!hit) return [];
|
|
chain.unshift(hit.username);
|
|
if (cur.toString() === rootKey) {
|
|
reachedRoot = true;
|
|
break;
|
|
}
|
|
cur = hit.parentAgentId;
|
|
}
|
|
return reachedRoot ? chain : [];
|
|
};
|
|
|
|
return new Map(agentIds.map((id) => [id.toString(), build(id)]));
|
|
}
|
|
|
|
private async loadAncestorProfileCache(agentIds: bigint[], stopBeforeParentOf?: bigint) {
|
|
const cache = new Map<string, { username: string; parentAgentId: bigint | null }>();
|
|
const pending = new Set(agentIds);
|
|
if (stopBeforeParentOf) pending.add(stopBeforeParentOf);
|
|
const stopKey = stopBeforeParentOf?.toString();
|
|
|
|
while (pending.size > 0) {
|
|
const batch = [...pending];
|
|
pending.clear();
|
|
const profiles = await this.prisma.agentProfile.findMany({
|
|
where: { userId: { in: batch } },
|
|
select: {
|
|
userId: true,
|
|
parentAgentId: true,
|
|
user: { select: { username: true } },
|
|
},
|
|
});
|
|
for (const profile of profiles) {
|
|
cache.set(profile.userId.toString(), {
|
|
username: profile.user.username,
|
|
parentAgentId: profile.parentAgentId,
|
|
});
|
|
if (
|
|
profile.parentAgentId &&
|
|
profile.parentAgentId.toString() !== stopKey &&
|
|
!cache.has(profile.parentAgentId.toString())
|
|
) {
|
|
pending.add(profile.parentAgentId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return cache;
|
|
}
|
|
|
|
private async findSubtreeClosureRows(agentId: bigint) {
|
|
return this.prisma.agentClosure.findMany({
|
|
where: { ancestorId: agentId },
|
|
select: { descendantId: true, depth: true },
|
|
orderBy: [{ depth: 'asc' }, { descendantId: 'asc' }],
|
|
});
|
|
}
|
|
|
|
private idsFromSubtreeClosure(agentId: bigint, rows: ClosureRow[]) {
|
|
return uniqueBigints([agentId, ...rows.map((row) => row.descendantId)]);
|
|
}
|
|
|
|
private async getDirectChildAgentIds(agentId: bigint) {
|
|
const children = await this.prisma.agentProfile.findMany({
|
|
where: { parentAgentId: agentId },
|
|
select: { userId: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
return children.map((child) => child.userId);
|
|
}
|
|
|
|
private async getSubtreeAgentIdsFromProfiles(agentId: bigint) {
|
|
const ids: bigint[] = [];
|
|
const queue: bigint[] = [agentId];
|
|
const seen = new Set<string>();
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.shift()!;
|
|
const key = current.toString();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
ids.push(current);
|
|
|
|
const children = await this.prisma.agentProfile.findMany({
|
|
where: { parentAgentId: current },
|
|
select: { userId: true },
|
|
});
|
|
for (const child of children) {
|
|
queue.push(child.userId);
|
|
}
|
|
}
|
|
|
|
return ids;
|
|
}
|
|
}
|