feat: add query parameter to users/following and users/followers APIs for filtering
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
parent
6591199c05
commit
f40a74449d
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IsNull } from 'typeorm';
|
import { Brackets, IsNull } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
|
@ -12,6 +12,8 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
|
@ -79,6 +81,7 @@ export const paramDef = {
|
||||||
sinceDate: { type: 'integer' },
|
sinceDate: { type: 'integer' },
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
|
query: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -100,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private followingEntityService: FollowingEntityService,
|
private followingEntityService: FollowingEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||||
|
|
@ -138,6 +142,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.andWhere('following.followeeId = :userId', { userId: user.id })
|
.andWhere('following.followeeId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.follower', 'follower');
|
.innerJoinAndSelect('following.follower', 'follower');
|
||||||
|
|
||||||
|
if (ps.query) {
|
||||||
|
const searchQuery = ps.query;
|
||||||
|
const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1;
|
||||||
|
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.where('follower.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' });
|
||||||
|
|
||||||
|
if (isUsername) {
|
||||||
|
qb.orWhere('follower.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' });
|
||||||
|
} else if (this.userEntityService.validateLocalUsername(searchQuery)) {
|
||||||
|
qb.orWhere('follower.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const followings = await query
|
const followings = await query
|
||||||
.limit(ps.limit)
|
.limit(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IsNull } from 'typeorm';
|
import { Brackets, IsNull } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { birthdaySchema } from '@/models/User.js';
|
import { birthdaySchema } from '@/models/User.js';
|
||||||
|
|
@ -13,6 +13,8 @@ import { FollowingEntityService } from '@/core/entities/FollowingEntityService.j
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
|
@ -87,6 +89,7 @@ export const paramDef = {
|
||||||
untilDate: { type: 'integer' },
|
untilDate: { type: 'integer' },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: { ...birthdaySchema, nullable: true },
|
||||||
|
query: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -108,6 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private followingEntityService: FollowingEntityService,
|
private followingEntityService: FollowingEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const user = await this.usersRepository.findOneBy('userId' in ps
|
const user = await this.usersRepository.findOneBy('userId' in ps
|
||||||
|
|
@ -146,7 +150,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.andWhere('following.followerId = :userId', { userId: user.id })
|
.andWhere('following.followerId = :userId', { userId: user.id })
|
||||||
.innerJoinAndSelect('following.followee', 'followee');
|
.innerJoinAndSelect('following.followee', 'followee');
|
||||||
|
|
||||||
if (ps.birthday) {
|
// query takes priority over birthday
|
||||||
|
if (ps.query) {
|
||||||
|
const searchQuery = ps.query;
|
||||||
|
const isUsername = searchQuery.startsWith('@') && !searchQuery.includes(' ') && searchQuery.indexOf('@', 1) === -1;
|
||||||
|
|
||||||
|
query.andWhere(new Brackets(qb => {
|
||||||
|
qb.where('followee.name ILIKE :query', { query: '%' + sqlLikeEscape(searchQuery) + '%' });
|
||||||
|
|
||||||
|
if (isUsername) {
|
||||||
|
qb.orWhere('followee.usernameLower LIKE :username', { username: sqlLikeEscape(searchQuery.replace('@', '').toLowerCase()) + '%' });
|
||||||
|
} else if (this.userEntityService.validateLocalUsername(searchQuery)) {
|
||||||
|
qb.orWhere('followee.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(searchQuery.toLowerCase()) + '%' });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else if (ps.birthday) {
|
||||||
try {
|
try {
|
||||||
const birthday = ps.birthday.substring(5, 10);
|
const birthday = ps.birthday.substring(5, 10);
|
||||||
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ const followingPaginator = markRaw(new Paginator('users/following', {
|
||||||
computedParams: computed(() => ({
|
computedParams: computed(() => ({
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})),
|
})),
|
||||||
|
canSearch: true,
|
||||||
|
searchParamName: 'query',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const followersPaginator = markRaw(new Paginator('users/followers', {
|
const followersPaginator = markRaw(new Paginator('users/followers', {
|
||||||
|
|
@ -37,6 +39,8 @@ const followersPaginator = markRaw(new Paginator('users/followers', {
|
||||||
computedParams: computed(() => ({
|
computedParams: computed(() => ({
|
||||||
userId: props.user.id,
|
userId: props.user.id,
|
||||||
})),
|
})),
|
||||||
|
canSearch: true,
|
||||||
|
searchParamName: 'query',
|
||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34768,6 +34768,7 @@ export interface operations {
|
||||||
untilDate?: number;
|
untilDate?: number;
|
||||||
/** @default 10 */
|
/** @default 10 */
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
query?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -34848,6 +34849,7 @@ export interface operations {
|
||||||
/** @default 10 */
|
/** @default 10 */
|
||||||
limit?: number;
|
limit?: number;
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
|
query?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue