refactor: DriveFileEntityServiceとDriveFolderEntityServiceの複数件取得をリファクタ
This commit is contained in:
parent
404fca6c2d
commit
4687591eb0
|
|
@ -17,6 +17,7 @@ import { deepClone } from '@/misc/clone.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { uniqueByKey } from '@/misc/unique-by-key.js';
|
||||
import { UtilityService } from '../UtilityService.js';
|
||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
|
@ -226,6 +227,7 @@ export class DriveFileEntityService {
|
|||
options?: PackOptions,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
packedFolder?: Packed<'DriveFolder'>
|
||||
},
|
||||
): Promise<Packed<'DriveFile'> | null> {
|
||||
const opts = Object.assign({
|
||||
|
|
@ -250,9 +252,9 @@ export class DriveFileEntityService {
|
|||
thumbnailUrl: this.getThumbnailUrl(file),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
folder: opts.detail && file.folderId ? (hint?.packedFolder ?? this.driveFolderEntityService.pack(file.folderId, {
|
||||
detail: true,
|
||||
}) : null,
|
||||
})) : null,
|
||||
userId: file.userId,
|
||||
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
|
||||
});
|
||||
|
|
@ -263,10 +265,41 @@ export class DriveFileEntityService {
|
|||
files: MiDriveFile[],
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
|
||||
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) } : {})));
|
||||
// -- ユーザ情報の事前取得 --
|
||||
|
||||
let userMap: Map<string, Packed<'UserLite'>> | null = null;
|
||||
if (options?.withUser) {
|
||||
const users = files
|
||||
.map(({ user, userId }) => user ?? userId)
|
||||
.filter(x => x != null);
|
||||
|
||||
const uniqueUsers = uniqueByKey(users, (user) => typeof user === 'string' ? user : user.id);
|
||||
const packedUsers = await this.userEntityService.packMany(uniqueUsers);
|
||||
userMap = new Map(packedUsers.map(user => [user.id, user]));
|
||||
}
|
||||
|
||||
// -- フォルダ情報の事前取得 --
|
||||
|
||||
let folderMap: Map<string, Packed<'DriveFolder'>> | null = null;
|
||||
if (options?.detail) {
|
||||
const folders = files
|
||||
.map(({ folder, folderId }) => folder ?? folderId)
|
||||
.filter(x => x != null);
|
||||
|
||||
const uniqueFolders = uniqueByKey(folders, (folder) => typeof folder === 'string' ? folder : folder.id);
|
||||
const packedFolders = await this.driveFolderEntityService.packMany(uniqueFolders, { detail: true });
|
||||
folderMap = new Map(packedFolders.map(folder => [folder.id, folder]));
|
||||
}
|
||||
|
||||
const items = await Promise.all(files.map(f => this.packNullable(
|
||||
f,
|
||||
options,
|
||||
{
|
||||
packedUser: f.userId ? userMap?.get(f.userId) : undefined,
|
||||
packedFolder: f.folderId ? folderMap?.get(f.folderId) : undefined,
|
||||
},
|
||||
)));
|
||||
|
||||
return items.filter(x => x != null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import type { } from '@/models/Blocking.js';
|
|||
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { In } from 'typeorm';
|
||||
import { uniqueByKey } from '@/misc/unique-by-key.js';
|
||||
import { splitIdAndObjects } from '@/misc/split-id-and-objects.js';
|
||||
|
||||
@Injectable()
|
||||
export class DriveFolderEntityService {
|
||||
|
|
@ -32,12 +35,20 @@ export class DriveFolderEntityService {
|
|||
options?: {
|
||||
detail: boolean
|
||||
},
|
||||
hint?: {
|
||||
folderMap?: Map<string, MiDriveFolder>;
|
||||
foldersCountMap?: Map<string, number> | null;
|
||||
filesCountMap?: Map<string, number> | null;
|
||||
parentPacker?: (id: string) => Promise<Packed<'DriveFolder'>>;
|
||||
},
|
||||
): Promise<Packed<'DriveFolder'>> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
}, options);
|
||||
|
||||
const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
|
||||
const folder = typeof src === 'object'
|
||||
? src
|
||||
: hint?.folderMap?.get(src) ?? await this.driveFoldersRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: folder.id,
|
||||
|
|
@ -46,20 +57,141 @@ export class DriveFolderEntityService {
|
|||
parentId: folder.parentId,
|
||||
|
||||
...(opts.detail ? {
|
||||
foldersCount: this.driveFoldersRepository.countBy({
|
||||
parentId: folder.id,
|
||||
}),
|
||||
filesCount: this.driveFilesRepository.countBy({
|
||||
folderId: folder.id,
|
||||
}),
|
||||
foldersCount: hint?.foldersCountMap?.get(folder.id)
|
||||
?? this.driveFoldersRepository.countBy({
|
||||
parentId: folder.id,
|
||||
}),
|
||||
filesCount: hint?.filesCountMap?.get(folder.id)
|
||||
?? this.driveFilesRepository.countBy({
|
||||
folderId: folder.id,
|
||||
}),
|
||||
|
||||
...(folder.parentId ? {
|
||||
parent: this.pack(folder.parentId, {
|
||||
detail: true,
|
||||
}),
|
||||
parent: hint?.parentPacker
|
||||
? hint.parentPacker(folder.parentId)
|
||||
: this.pack(folder.parentId, { detail: true }, hint),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async packMany(
|
||||
src: Array<MiDriveFolder['id'] | MiDriveFolder>,
|
||||
options?: {
|
||||
detail: boolean
|
||||
},
|
||||
): Promise<Array<Packed<'DriveFolder'>>> {
|
||||
/**
|
||||
* 重複を除去しつつ、必要なDriveFolderオブジェクトをすべて取得する
|
||||
*/
|
||||
const collectUniqueObjects = async (src: Array<MiDriveFolder['id'] | MiDriveFolder>) => {
|
||||
const uniqueSrc = uniqueByKey(
|
||||
src,
|
||||
(s) => typeof s === 'string' ? s : s.id,
|
||||
);
|
||||
const { ids, objects } = splitIdAndObjects(uniqueSrc);
|
||||
|
||||
const uniqueObjects = new Map<string, MiDriveFolder>(objects.map(s => [s.id, s]));
|
||||
const needsFetchIds = ids.filter(id => !uniqueObjects.has(id));
|
||||
|
||||
if (needsFetchIds.length > 0) {
|
||||
const fetchedObjects = await this.driveFoldersRepository.find({
|
||||
where: {
|
||||
id: In(needsFetchIds),
|
||||
},
|
||||
});
|
||||
for (const obj of fetchedObjects) {
|
||||
uniqueObjects.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueObjects;
|
||||
};
|
||||
|
||||
/**
|
||||
* 親フォルダーを再帰的に収集する
|
||||
*/
|
||||
const collectAncestors = async (folderMap: Map<string, MiDriveFolder>) => {
|
||||
for (;;) {
|
||||
const parentIds = new Set<string>();
|
||||
for (const folder of folderMap.values()) {
|
||||
if (folder.parentId != null && !folderMap.has(folder.parentId)) {
|
||||
parentIds.add(folder.parentId);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIds.size === 0) break;
|
||||
|
||||
const fetchedParents = await this.driveFoldersRepository.find({
|
||||
where: {
|
||||
id: In([...parentIds]),
|
||||
},
|
||||
});
|
||||
|
||||
if (fetchedParents.length === 0) break;
|
||||
|
||||
for (const parent of fetchedParents) {
|
||||
folderMap.set(parent.id, parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
}, options);
|
||||
|
||||
const folderMap = await collectUniqueObjects(src);
|
||||
|
||||
let foldersCountMap: Map<string, number> | null = null;
|
||||
let filesCountMap: Map<string, number> | null = null;
|
||||
if (opts.detail) {
|
||||
await collectAncestors(folderMap);
|
||||
|
||||
const ids = [...folderMap.keys()];
|
||||
if (ids.length > 0) {
|
||||
const folderCounts = await this.driveFoldersRepository.createQueryBuilder('folder')
|
||||
.select('folder.parentId', 'parentId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('folder.parentId IN (:...ids)', { ids })
|
||||
.groupBy('folder.parentId')
|
||||
.getRawMany<{ parentId: string; count: number }>();
|
||||
|
||||
const fileCounts = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.select('file.folderId', 'folderId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('file.folderId IN (:...ids)', { ids })
|
||||
.groupBy('file.folderId')
|
||||
.getRawMany<{ folderId: string; count: number }>();
|
||||
|
||||
foldersCountMap = new Map(folderCounts.map(row => [row.parentId, row.count]));
|
||||
filesCountMap = new Map(fileCounts.map(row => [row.folderId, row.count]));
|
||||
} else {
|
||||
foldersCountMap = new Map();
|
||||
filesCountMap = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
const packedMap = new Map<string, Promise<Packed<'DriveFolder'>>>();
|
||||
const packFromId = (id: string): Promise<Packed<'DriveFolder'>> => {
|
||||
const cached = packedMap.get(id);
|
||||
if (cached) return cached;
|
||||
|
||||
const folder = folderMap.get(id);
|
||||
if (!folder) {
|
||||
throw new Error(`DriveFolder not found: ${id}`);
|
||||
}
|
||||
|
||||
const packedPromise = this.pack(folder, options, {
|
||||
folderMap,
|
||||
foldersCountMap,
|
||||
filesCountMap,
|
||||
parentPacker: packFromId,
|
||||
});
|
||||
packedMap.set(id, packedPromise);
|
||||
|
||||
return packedPromise;
|
||||
};
|
||||
|
||||
return Promise.all(src.map(s => packFromId(typeof s === 'string' ? s : s.id)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* idとオブジェクトを分離する
|
||||
* @param input idまたはオブジェクトの配列
|
||||
* @returns idの配列とオブジェクトの配列
|
||||
*/
|
||||
export function splitIdAndObjects<T extends { id: string }>(input: (T | string)[]): { ids: string[]; objects: T[] } {
|
||||
const ids: string[] = [];
|
||||
const objects : T[] = [];
|
||||
|
||||
for (const item of input) {
|
||||
if (typeof item === 'string') {
|
||||
ids.push(item);
|
||||
} else {
|
||||
objects.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ids,
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* itemsの中でkey関数が返す値が重複しないようにした配列を返す
|
||||
* @param items 重複を除去したい配列
|
||||
* @param key 重複判定に使うキーを返す関数
|
||||
* @returns 重複を除去した配列
|
||||
*/
|
||||
export function uniqueByKey<TItem, TKey = string>(items: Iterable<TItem>, key: (item: TItem) => TKey): TItem[] {
|
||||
const map = new Map<TKey, TItem>();
|
||||
for (const item of items) {
|
||||
const k = key(item);
|
||||
if (!map.has(k)) {
|
||||
map.set(k, item);
|
||||
}
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
Loading…
Reference in New Issue