1
0
forked from mirror/misskey

perf(backend): avoid N+1 selects from user table when packing many entities (#13911)

* perf(backend): avoid N+1 selects from `user` table when packing many entities

* perf(backend): use `packMany` instead of mapping to `pack`
This commit is contained in:
zyoshoka 2024-05-31 15:32:42 +09:00 committed by GitHub
parent 97be1a53ad
commit 514a65e453
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 268 additions and 87 deletions

View File

@ -10,6 +10,8 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -26,6 +28,11 @@ export class AbuseUserReportEntityService {
@bindThis @bindThis
public async pack( public async pack(
src: MiAbuseUserReport['id'] | MiAbuseUserReport, src: MiAbuseUserReport['id'] | MiAbuseUserReport,
hint?: {
packedReporter?: Packed<'UserDetailedNotMe'>,
packedTargetUser?: Packed<'UserDetailedNotMe'>,
packedAssignee?: Packed<'UserDetailedNotMe'>,
},
) { ) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src }); const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
@ -37,13 +44,13 @@ export class AbuseUserReportEntityService {
reporterId: report.reporterId, reporterId: report.reporterId,
targetUserId: report.targetUserId, targetUserId: report.targetUserId,
assigneeId: report.assigneeId, assigneeId: report.assigneeId,
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, { reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
@ -51,9 +58,24 @@ export class AbuseUserReportEntityService {
} }
@bindThis @bindThis
public packMany( public async packMany(
reports: any[], reports: MiAbuseUserReport[],
) { ) {
return Promise.all(reports.map(x => this.pack(x))); const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
const _userMap = await this.userEntityService.packMany(
[..._reporters, ..._targetUsers, ..._assignees],
null,
{ schema: 'UserDetailedNotMe' },
).then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
reports.map(report => {
const packedReporter = _userMap.get(report.reporterId);
const packedTargetUser = _userMap.get(report.targetUserId);
const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
}),
);
} }
} }

View File

@ -29,6 +29,9 @@ export class BlockingEntityService {
public async pack( public async pack(
src: MiBlocking['id'] | MiBlocking, src: MiBlocking['id'] | MiBlocking,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
blockee?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Blocking'>> { ): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src }); const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
@ -36,17 +39,20 @@ export class BlockingEntityService {
id: blocking.id, id: blocking.id,
createdAt: this.idService.parse(blocking.id).date.toISOString(), createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId, blockeeId: blocking.blockeeId,
blockee: this.userEntityService.pack(blocking.blockeeId, me, { blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
blockings: any[], blockings: MiBlocking[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
return Promise.all(blockings.map(x => this.pack(x, me))); const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
} }
} }

View File

@ -35,6 +35,9 @@ export class ClipEntityService {
public async pack( public async pack(
src: MiClip['id'] | MiClip, src: MiClip['id'] | MiClip,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Clip'>> { ): Promise<Packed<'Clip'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src }); const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
@ -44,7 +47,7 @@ export class ClipEntityService {
createdAt: this.idService.parse(clip.id).date.toISOString(), createdAt: this.idService.parse(clip.id).date.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null, lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId, userId: clip.userId,
user: this.userEntityService.pack(clip.user ?? clip.userId), user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name, name: clip.name,
description: clip.description, description: clip.description,
isPublic: clip.isPublic, isPublic: clip.isPublic,
@ -55,11 +58,14 @@ export class ClipEntityService {
} }
@bindThis @bindThis
public packMany( public async packMany(
clips: MiClip[], clips: MiClip[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(clips.map(x => this.pack(x, me))); const _users = clips.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
} }
} }

View File

@ -222,6 +222,9 @@ export class DriveFileEntityService {
public async packNullable( public async packNullable(
src: MiDriveFile['id'] | MiDriveFile, src: MiDriveFile['id'] | MiDriveFile,
options?: PackOptions, options?: PackOptions,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'DriveFile'> | null> { ): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({ const opts = Object.assign({
detail: false, detail: false,
@ -249,7 +252,7 @@ export class DriveFileEntityService {
detail: true, detail: true,
}) : null, }) : null,
userId: file.userId, userId: file.userId,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null, user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
}); });
} }
@ -258,7 +261,10 @@ export class DriveFileEntityService {
files: MiDriveFile[], files: MiDriveFile[],
options?: PackOptions, options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> { ): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options))); const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull); return items.filter(isNotNull);
} }

View File

@ -33,6 +33,9 @@ export class FlashEntityService {
public async pack( public async pack(
src: MiFlash['id'] | MiFlash, src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Flash'>> { ): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
@ -42,7 +45,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(), createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(), updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId, userId: flash.userId,
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title, title: flash.title,
summary: flash.summary, summary: flash.summary,
script: flash.script, script: flash.script,
@ -52,11 +55,14 @@ export class FlashEntityService {
} }
@bindThis @bindThis
public packMany( public async packMany(
flashs: MiFlash[], flashes: MiFlash[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(flashs.map(x => this.pack(x, me))); const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
} }
} }

View File

@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiFollowRequest } from '@/models/FollowRequest.js'; import type { MiFollowRequest } from '@/models/FollowRequest.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -26,14 +27,36 @@ export class FollowRequestEntityService {
public async pack( public async pack(
src: MiFollowRequest['id'] | MiFollowRequest, src: MiFollowRequest['id'] | MiFollowRequest,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedFollower?: Packed<'UserLite'>,
packedFollowee?: Packed<'UserLite'>,
},
) { ) {
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src }); const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
return { return {
id: request.id, id: request.id,
follower: await this.userEntityService.pack(request.followerId, me), follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
followee: await this.userEntityService.pack(request.followeeId, me), followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
}; };
} }
@bindThis
public async packMany(
requests: MiFollowRequest[],
me?: { id: MiUser['id'] } | null | undefined,
) {
const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
requests.map(req => {
const packedFollower = _userMap.get(req.followerId);
const packedFollowee = _userMap.get(req.followeeId);
return this.pack(req, me, { packedFollower, packedFollowee });
}),
);
}
} }

View File

@ -78,6 +78,10 @@ export class FollowingEntityService {
populateFollowee?: boolean; populateFollowee?: boolean;
populateFollower?: boolean; populateFollower?: boolean;
}, },
hint?: {
packedFollowee?: Packed<'UserDetailedNotMe'>,
packedFollower?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Following'>> { ): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src }); const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
@ -88,25 +92,35 @@ export class FollowingEntityService {
createdAt: this.idService.parse(following.id).date.toISOString(), createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId, followeeId: following.followeeId,
followerId: following.followerId, followerId: following.followerId,
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, { followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}) : undefined, }) : undefined,
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, { follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}) : undefined, }) : undefined,
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
followings: any[], followings: MiFollowing[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
opts?: { opts?: {
populateFollowee?: boolean; populateFollowee?: boolean;
populateFollower?: boolean; populateFollower?: boolean;
}, },
) { ) {
return Promise.all(followings.map(x => this.pack(x, me, opts))); const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
followings.map(following => {
const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
return this.pack(following, me, opts, { packedFollowee, packedFollower });
}),
);
} }
} }

View File

@ -35,6 +35,9 @@ export class GalleryPostEntityService {
public async pack( public async pack(
src: MiGalleryPost['id'] | MiGalleryPost, src: MiGalleryPost['id'] | MiGalleryPost,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'GalleryPost'>> { ): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src }); const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
@ -44,7 +47,7 @@ export class GalleryPostEntityService {
createdAt: this.idService.parse(post.id).date.toISOString(), createdAt: this.idService.parse(post.id).date.toISOString(),
updatedAt: post.updatedAt.toISOString(), updatedAt: post.updatedAt.toISOString(),
userId: post.userId, userId: post.userId,
user: this.userEntityService.pack(post.user ?? post.userId, me), user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title, title: post.title,
description: post.description, description: post.description,
fileIds: post.fileIds, fileIds: post.fileIds,
@ -58,11 +61,14 @@ export class GalleryPostEntityService {
} }
@bindThis @bindThis
public packMany( public async packMany(
posts: MiGalleryPost[], posts: MiGalleryPost[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(posts.map(x => this.pack(x, me))); const _users = posts.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
} }
} }

View File

@ -12,6 +12,7 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -29,6 +30,10 @@ export class InviteCodeEntityService {
public async pack( public async pack(
src: MiRegistrationTicket['id'] | MiRegistrationTicket, src: MiRegistrationTicket['id'] | MiRegistrationTicket,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedCreatedBy?: Packed<'UserLite'>,
packedUsedBy?: Packed<'UserLite'>,
},
): Promise<Packed<'InviteCode'>> { ): Promise<Packed<'InviteCode'>> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({ const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: { where: {
@ -42,18 +47,28 @@ export class InviteCodeEntityService {
code: target.code, code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null, expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: this.idService.parse(target.id).date.toISOString(), createdAt: this.idService.parse(target.id).date.toISOString(),
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null, createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null, usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null, usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt, used: !!target.usedAt,
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
targets: any[], tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
return Promise.all(targets.map(x => this.pack(x, me))); const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
tickets.map(ticket => {
const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
}),
);
} }
} }

View File

@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
import type { ModerationLogsRepository } from '@/models/_.js'; import type { ModerationLogsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/Blocking.js'; import type { } from '@/models/Blocking.js';
import type { MiModerationLog } from '@/models/ModerationLog.js'; import { MiModerationLog } from '@/models/ModerationLog.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -27,6 +28,9 @@ export class ModerationLogEntityService {
@bindThis @bindThis
public async pack( public async pack(
src: MiModerationLog['id'] | MiModerationLog, src: MiModerationLog['id'] | MiModerationLog,
hint?: {
packedUser?: Packed<'UserDetailedNotMe'>,
},
) { ) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src }); const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
@ -36,17 +40,20 @@ export class ModerationLogEntityService {
type: log.type, type: log.type,
info: log.info, info: log.info,
userId: log.userId, userId: log.userId,
user: this.userEntityService.pack(log.user ?? log.userId, null, { user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
reports: any[], reports: MiModerationLog[],
) { ) {
return Promise.all(reports.map(x => this.pack(x))); const _users = reports.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
} }
} }

View File

@ -30,6 +30,9 @@ export class MutingEntityService {
public async pack( public async pack(
src: MiMuting['id'] | MiMuting, src: MiMuting['id'] | MiMuting,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedMutee?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Muting'>> { ): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src }); const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
@ -38,18 +41,21 @@ export class MutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(), createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId, muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, { mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
mutings: any[], mutings: MiMuting[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
return Promise.all(mutings.map(x => this.pack(x, me))); const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
} }
} }

View File

@ -290,6 +290,7 @@ export class NoteEntityService implements OnModuleInit {
_hint_?: { _hint_?: {
myReactions: Map<MiNote['id'], string | null>; myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
}; };
}, },
): Promise<Packed<'Note'>> { ): Promise<Packed<'Note'>> {
@ -319,12 +320,13 @@ export class NoteEntityService implements OnModuleInit {
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles; const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'Note'> = await awaitAll({ const packed: Packed<'Note'> = await awaitAll({
id: note.id, id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(), createdAt: this.idService.parse(note.id).date.toISOString(),
userId: note.userId, userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me), user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text, text: text,
cw: note.cw, cw: note.cw,
visibility: note.visibility, visibility: note.visibility,
@ -449,12 +451,20 @@ export class NoteEntityService implements OnModuleInit {
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [
...notes.map(({ user, userId }) => user ?? userId),
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
];
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(notes.map(n => this.pack(n, me, { return await Promise.all(notes.map(n => this.pack(n, me, {
...options, ...options,
_hint_: { _hint_: {
myReactions: myReactionsMap, myReactions: myReactionsMap,
packedFiles, packedFiles,
packedUsers,
}, },
}))); })));
} }

View File

@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
options?: { options?: {
withNote: boolean; withNote: boolean;
}, },
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReaction'>> { ): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({ const opts = Object.assign({
withNote: false, withNote: false,
@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
return { return {
id: reaction.id, id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(), createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me), user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction), type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? { ...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me), note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
const opts = Object.assign({ const opts = Object.assign({
withNote: false, withNote: false,
}, options); }, options);
const _users = reactions.map(({ user, userId }) => user ?? userId);
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts))); const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
} }
} }

View File

@ -40,6 +40,9 @@ export class PageEntityService {
public async pack( public async pack(
src: MiPage['id'] | MiPage, src: MiPage['id'] | MiPage,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Page'>> { ): Promise<Packed<'Page'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src }); const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
@ -91,7 +94,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(), createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(), updatedAt: page.updatedAt.toISOString(),
userId: page.userId, userId: page.userId,
user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content, content: page.content,
variables: page.variables, variables: page.variables,
title: page.title, title: page.title,
@ -110,11 +113,14 @@ export class PageEntityService {
} }
@bindThis @bindThis
public packMany( public async packMany(
pages: MiPage[], pages: MiPage[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
) { ) {
return Promise.all(pages.map(x => this.pack(x, me))); const _users = pages.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
} }
} }

View File

@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
public async pack( public async pack(
src: MiRenoteMuting['id'] | MiRenoteMuting, src: MiRenoteMuting['id'] | MiRenoteMuting,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedMutee?: Packed<'UserDetailedNotMe'>
},
): Promise<Packed<'RenoteMuting'>> { ): Promise<Packed<'RenoteMuting'>> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src }); const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
id: muting.id, id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(), createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId, muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, { mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}), }),
}); });
} }
@bindThis @bindThis
public packMany( public async packMany(
mutings: any[], mutings: MiRenoteMuting[],
me: { id: MiUser['id'] }, me: { id: MiUser['id'] },
) { ) {
return Promise.all(mutings.map(x => this.pack(x, me))); const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
} }
} }

View File

@ -28,13 +28,15 @@ export class ReversiGameEntityService {
@bindThis @bindThis
public async packDetail( public async packDetail(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
hint?: {
packedUser1?: Packed<'UserLite'>,
packedUser2?: Packed<'UserLite'>,
},
): Promise<Packed<'ReversiGameDetailed'>> { ): Promise<Packed<'ReversiGameDetailed'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
this.userEntityService.pack(game.user1 ?? game.user1Id), const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
return await awaitAll({ return await awaitAll({
id: game.id, id: game.id,
@ -49,10 +51,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready, user2Ready: game.user2Ready,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: users[0], user1,
user2: users[1], user2,
winnerId: game.winnerId, winnerId: game.winnerId,
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null, winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -68,22 +70,35 @@ export class ReversiGameEntityService {
} }
@bindThis @bindThis
public packDetailMany( public async packDetailMany(
xs: MiReversiGame[], games: MiReversiGame[],
) { ) {
return Promise.all(xs.map(x => this.packDetail(x))); const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
games.map(game => {
return this.packDetail(game, {
packedUser1: _userMap.get(game.user1Id),
packedUser2: _userMap.get(game.user2Id),
});
}),
);
} }
@bindThis @bindThis
public async packLite( public async packLite(
src: MiReversiGame['id'] | MiReversiGame, src: MiReversiGame['id'] | MiReversiGame,
hint?: {
packedUser1?: Packed<'UserLite'>,
packedUser2?: Packed<'UserLite'>,
},
): Promise<Packed<'ReversiGameLite'>> { ): Promise<Packed<'ReversiGameLite'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([ const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
this.userEntityService.pack(game.user1 ?? game.user1Id), const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
return await awaitAll({ return await awaitAll({
id: game.id, id: game.id,
@ -94,10 +109,10 @@ export class ReversiGameEntityService {
isEnded: game.isEnded, isEnded: game.isEnded,
user1Id: game.user1Id, user1Id: game.user1Id,
user2Id: game.user2Id, user2Id: game.user2Id,
user1: users[0], user1,
user2: users[1], user2,
winnerId: game.winnerId, winnerId: game.winnerId,
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null, winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId, surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId, timeoutUserId: game.timeoutUserId,
black: game.black, black: game.black,
@ -111,10 +126,21 @@ export class ReversiGameEntityService {
} }
@bindThis @bindThis
public packLiteMany( public async packLiteMany(
xs: MiReversiGame[], games: MiReversiGame[],
) { ) {
return Promise.all(xs.map(x => this.packLite(x))); const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
games.map(game => {
return this.packLite(game, {
packedUser1: _userMap.get(game.user1Id),
packedUser2: _userMap.get(game.user2Id),
});
}),
);
} }
} }

View File

@ -50,11 +50,14 @@ export class UserListEntityService {
public async packMembershipsMany( public async packMembershipsMany(
memberships: MiUserListMembership[], memberships: MiUserListMembership[],
) { ) {
const _users = memberships.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(async x => ({ return Promise.all(memberships.map(async x => ({
id: x.id, id: x.id,
createdAt: this.idService.parse(x.id).date.toISOString(), createdAt: this.idService.parse(x.id).date.toISOString(),
userId: x.userId, userId: x.userId,
user: await this.userEntityService.pack(x.userId), user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
withReplies: x.withReplies, withReplies: x.withReplies,
}))); })));
} }

View File

@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({ return await Promise.all(assigns.map(async assign => ({
id: assign.id, id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(), createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null, expiresAt: assign.expiresAt?.toISOString() ?? null,
}))); })));
}); });

View File

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(), folderId: ps.folderId ?? IsNull(),
}); });
return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true }))); return await this.driveFileEntityService.packMany(files, { self: true });
}); });
} }
} }

View File

@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); return await this.followRequestEntityService.packMany(requests, me);
}); });
} }
} }

View File

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany(); const reactions = await query.limit(ps.limit).getMany();
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); return await this.noteReactionEntityService.packMany(reactions, me);
}); });
} }
} }

View File

@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({ return await Promise.all(assigns.map(async assign => ({
id: assign.id, id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
}))); })));
}); });
} }

View File

@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users // Extract top replied users
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights) // Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }), .then(users => new Map(users.map(u => [u.id, u])));
weight: repliedUsers[user] / peak, const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
weight: repliedUsers[userId] / peak,
}))); })));
return repliesObj; return repliesObj;

View File

@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user != null) _users.push(user); if (user != null) _users.push(user);
} }
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
schema: 'UserDetailed', .then(users => new Map(users.map(u => [u.id, u])));
}))); return _users.map(u => _userMap.get(u.id)!);
} else { } else {
// Lookup user // Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') { if (typeof ps.host === 'string' && typeof ps.username === 'string') {