enhance(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットをリファクタリング
This commit is contained in:
parent
9723d01277
commit
24652b9364
18 changed files with 434 additions and 94 deletions
15
packages/backend/migration/1711478468155-birthday-index.js
Normal file
15
packages/backend/migration/1711478468155-birthday-index.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export class BirthdayIndex1711478468155 {
|
||||
name = 'BirthdayIndex1711478468155'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
|
||||
await queryRunner.query(`CREATE OR REPLACE FUNCTION get_birthday_date(birthday TEXT) RETURNS SMALLINT AS $$ BEGIN RETURN CAST((SUBSTR(birthday, 6, 2) || SUBSTR(birthday, 9, 2)) AS SMALLINT); END; $$ LANGUAGE plpgsql IMMUTABLE;`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_USERPROFILE_BIRTHDAY_DATE" ON "user_profile" (get_birthday_date("birthday"))`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (substr("birthday", 6, 5))`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_USERPROFILE_BIRTHDAY_DATE"`);
|
||||
await queryRunner.query(`DROP FUNCTION IF EXISTS get_birthday_date(birthday TEXT)`);
|
||||
}
|
||||
}
|
||||
|
|
@ -642,8 +642,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
|
||||
// -- 特に前提条件のない値群を取得
|
||||
|
||||
const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||
.then(profiles => new Map(profiles.map(p => [p.userId, p])));
|
||||
const profilesMap = (options?.schema !== 'UserLite') ? await this.userProfilesRepository.findBy({ userId: In(_userIds) })
|
||||
.then(profiles => new Map(profiles.map(p => [p.userId, p]))) : undefined;
|
||||
|
||||
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
||||
|
||||
|
|
@ -680,7 +680,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
}
|
||||
}
|
||||
|
||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
|
|||
import * as ep___users_followers from './endpoints/users/followers.js';
|
||||
import * as ep___users_following from './endpoints/users/following.js';
|
||||
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
||||
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
|
||||
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
||||
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
||||
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
||||
|
|
@ -733,6 +734,7 @@ const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users
|
|||
const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
|
||||
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
|
||||
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
|
||||
const $users_getFollowingBirthdayUsers: Provider = { provide: 'ep:users/get-following-birthday-users', useClass: ep___users_getFollowingBirthdayUsers.default };
|
||||
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
|
||||
const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
|
||||
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
|
||||
|
|
@ -1122,6 +1124,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$users_followers,
|
||||
$users_following,
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
|
|
@ -1503,6 +1506,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$users_followers,
|
||||
$users_following,
|
||||
$users_gallery_posts,
|
||||
$users_getFollowingBirthdayUsers,
|
||||
$users_getFrequentlyRepliedUsers,
|
||||
$users_featuredNotes,
|
||||
$users_lists_create,
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@ import * as ep___users_clips from './endpoints/users/clips.js';
|
|||
import * as ep___users_followers from './endpoints/users/followers.js';
|
||||
import * as ep___users_following from './endpoints/users/following.js';
|
||||
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
||||
import * as ep___users_getFollowingBirthdayUsers from './endpoints/users/get-following-birthday-users.js';
|
||||
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
|
||||
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
|
||||
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
||||
|
|
@ -731,6 +732,7 @@ const eps = [
|
|||
['users/followers', ep___users_followers],
|
||||
['users/following', ep___users_following],
|
||||
['users/gallery/posts', ep___users_gallery_posts],
|
||||
['users/get-following-birthday-users', ep___users_getFollowingBirthdayUsers],
|
||||
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
|
||||
['users/featured-notes', ep___users_featuredNotes],
|
||||
['users/lists/create', ep___users_lists_create],
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ export const paramDef = {
|
|||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
|
||||
birthday: { ...birthdaySchema, nullable: true },
|
||||
birthday: {
|
||||
...birthdaySchema, nullable: true,
|
||||
description: '@deprecated use get-following-birthday-users instead.',
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
|
|
@ -126,14 +129,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
// @deprecated use get-following-birthday-users instead.
|
||||
if (ps.birthday) {
|
||||
try {
|
||||
const birthday = ps.birthday.substring(5, 10);
|
||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
||||
birthdayUserQuery.select('user_profile.userId')
|
||||
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
|
||||
query.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||
|
||||
query.andWhere(`following.followeeId IN (${ birthdayUserQuery.getQuery() })`);
|
||||
try {
|
||||
const birthday = ps.birthday.split('-');
|
||||
birthday.shift(); // 年の部分を削除
|
||||
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: parseInt(birthday.join('')) });
|
||||
} catch (err) {
|
||||
throw new ApiError(meta.errors.birthdayInvalid);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type {
|
||||
FollowingsRepository,
|
||||
UserProfilesRepository,
|
||||
} from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: true,
|
||||
kind: 'read:account',
|
||||
|
||||
description: 'Find users who have a birthday on the specified range.',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
birthday: {
|
||||
type: 'string', format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
birthday: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||
begin: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||
},
|
||||
required: ['month', 'day'],
|
||||
},
|
||||
end: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
month: { type: 'integer', minimum: 1, maximum: 12 },
|
||||
day: { type: 'integer', minimum: 1, maximum: 31 },
|
||||
},
|
||||
required: ['month', 'day'],
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['month', 'day'] },
|
||||
{ required: ['begin', 'end'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ['birthday'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.followingsRepository
|
||||
.createQueryBuilder('following')
|
||||
.andWhere('following.followerId = :userId', { userId: me.id })
|
||||
.innerJoin(this.userProfilesRepository.metadata.targetName, 'followeeProfile', 'followeeProfile.userId = following.followeeId');
|
||||
|
||||
if (Object.hasOwn(ps.birthday, 'begin') && Object.hasOwn(ps.birthday, 'end')) {
|
||||
const { begin, end } = ps.birthday as { begin: { month: number; day: number }; end: { month: number; day: number }; };
|
||||
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :begin AND :end', { begin: begin.month * 100 + begin.day, end: end.month * 100 + end.day });
|
||||
} else {
|
||||
const { month, day } = ps.birthday as { month: number; day: number };
|
||||
// なぜか get_birthday_date() = :birthday だとインデックスが効かないので、BETWEEN で対応
|
||||
query.andWhere('get_birthday_date(followeeProfile.birthday) BETWEEN :birthday AND :birthday', { birthday: month * 100 + day });
|
||||
}
|
||||
|
||||
query.select('following.followeeId', 'user_id');
|
||||
query.addSelect('get_birthday_date(followeeProfile.birthday)', 'birthday_date');
|
||||
query.orderBy('birthday_date', 'ASC');
|
||||
|
||||
const birthdayUsers = await query
|
||||
.offset(ps.offset).limit(ps.limit)
|
||||
.getRawMany<{ birthday_date: number; user_id: string }>();
|
||||
|
||||
const users = new Map<string, Packed<'UserLite'>>((
|
||||
await this.userEntityService.packMany(
|
||||
birthdayUsers.map(u => u.user_id),
|
||||
me,
|
||||
{ schema: 'UserLite' },
|
||||
)
|
||||
).map(u => [u.id, u]));
|
||||
|
||||
return birthdayUsers
|
||||
.map(item => {
|
||||
const birthday = new Date();
|
||||
birthday.setMonth(Math.floor(item.birthday_date / 100) - 1);
|
||||
birthday.setDate(item.birthday_date % 100);
|
||||
birthday.setHours(0, 0, 0, 0);
|
||||
if (birthday.getTime() < Date.now()) birthday.setFullYear(new Date().getFullYear() + 1);
|
||||
return { birthday: birthday.toISOString(), user: users.get(item.user_id) };
|
||||
})
|
||||
.filter(item => item.user !== undefined)
|
||||
.map(item => item as { birthday: string; user: Packed<'UserLite'> });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -27,10 +27,21 @@ export const meta = {
|
|||
res: {
|
||||
optional: false, nullable: false,
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
ref: 'UserDetailed',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
|
|
@ -71,6 +82,7 @@ export const paramDef = {
|
|||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
detailed: { type: 'boolean', default: true },
|
||||
},
|
||||
anyOf: [
|
||||
{ required: ['userId'] },
|
||||
|
|
@ -117,7 +129,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
return await this.userEntityService.packMany(_users, me, {
|
||||
schema: 'UserDetailed',
|
||||
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||
});
|
||||
} else {
|
||||
// Lookup user
|
||||
|
|
@ -147,7 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
return await this.userEntityService.pack(user, me, {
|
||||
schema: 'UserDetailed',
|
||||
schema: ps.detailed ? 'UserDetailed' : 'UserLite',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue