Refactor search query options

This commit is contained in:
Chocobozzz 2021-07-29 11:54:38 +02:00
parent b033851fb5
commit d688602710
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
15 changed files with 261 additions and 121 deletions

View File

@ -1,6 +1,6 @@
import * as express from 'express'
import { pickCommonVideoQuery } from '@server/helpers/query'
import { getServerActor } from '@server/models/application/application'
import { VideosWithSearchCommonQuery } from '@shared/models'
import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
import { getFormattedObjects } from '../../helpers/utils'
import { JobQueue } from '../../lib/job-queue'
@ -159,27 +159,19 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
const account = res.locals.account
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
const countVideos = getCountVideos(req)
const query = req.query as VideosWithSearchCommonQuery
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
followerActorId,
start: query.start,
count: query.count,
sort: query.sort,
search: req.query.search,
includeLocalVideos: true,
categoryOneOf: query.categoryOneOf,
licenceOneOf: query.licenceOneOf,
languageOneOf: query.languageOneOf,
tagsOneOf: query.tagsOneOf,
tagsAllOf: query.tagsAllOf,
filter: query.filter,
isLive: query.isLive,
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos,
search: query.search
countVideos
}, 'filter:api.accounts.videos.list.params')
const resultList = await Hooks.wrapPromiseFun(

View File

@ -1,5 +1,6 @@
import * as express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils'
import { pickSearchChannelQuery } from '@server/helpers/query'
import { doJSONRequest } from '@server/helpers/requests'
import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants'
@ -7,7 +8,7 @@ import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
import { getServerActor } from '@server/models/application/application'
import { HttpStatusCode, ResultList, VideoChannel } from '@shared/models'
import { VideoChannelsSearchQuery } from '../../../../shared/models/search'
import { VideoChannelsSearchQueryAfterSanitize } from '../../../../shared/models/search'
import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
@ -45,7 +46,7 @@ export { searchChannelsRouter }
// ---------------------------------------------------------------------------
function searchVideoChannels (req: express.Request, res: express.Response) {
const query: VideoChannelsSearchQuery = req.query
const query = pickSearchChannelQuery(req.query)
let search = query.search || ''
const parts = search.split('@')
@ -66,7 +67,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
return searchVideoChannelsDB(query, res)
}
async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
async function searchVideoChannelsIndex (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
@ -90,17 +91,13 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
}
}
async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
actorId: serverActor.id,
search: query.search,
start: query.start,
count: query.count,
sort: query.sort,
host: query.host,
handles: query.handles
...query,
actorId: serverActor.id
}, 'filter:api.search.video-channels.local.list.params')
const resultList = await Hooks.wrapPromiseFun(

View File

@ -2,6 +2,7 @@ import * as express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils'
import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils'
import { logger } from '@server/helpers/logger'
import { pickSearchPlaylistQuery } from '@server/helpers/query'
import { doJSONRequest } from '@server/helpers/requests'
import { getFormattedObjects } from '@server/helpers/utils'
import { CONFIG } from '@server/initializers/config'
@ -12,7 +13,7 @@ import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@ser
import { getServerActor } from '@server/models/application/application'
import { VideoPlaylistModel } from '@server/models/video/video-playlist'
import { MVideoPlaylistFullSummary } from '@server/types/models'
import { HttpStatusCode, ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models'
import { HttpStatusCode, ResultList, VideoPlaylist, VideoPlaylistsSearchQueryAfterSanitize } from '@shared/models'
import {
asyncMiddleware,
openapiOperationDoc,
@ -44,7 +45,7 @@ export { searchPlaylistsRouter }
// ---------------------------------------------------------------------------
function searchVideoPlaylists (req: express.Request, res: express.Response) {
const query: VideoPlaylistsSearchQuery = req.query
const query = pickSearchPlaylistQuery(req.query)
const search = query.search
if (isURISearch(search)) return searchVideoPlaylistsURI(search, res)
@ -56,7 +57,7 @@ function searchVideoPlaylists (req: express.Request, res: express.Response) {
return searchVideoPlaylistsDB(query, res)
}
async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) {
async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params')
@ -80,17 +81,13 @@ async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res:
}
}
async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) {
async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) {
const serverActor = await getServerActor()
const apiOptions = await Hooks.wrapObject({
followerActorId: serverActor.id,
search: query.search,
start: query.start,
count: query.count,
sort: query.sort,
host: query.host,
uuids: query.uuids
...query,
followerActorId: serverActor.id
}, 'filter:api.search.video-playlists.local.list.params')
const resultList = await Hooks.wrapPromiseFun(

View File

@ -1,5 +1,6 @@
import * as express from 'express'
import { sanitizeUrl } from '@server/helpers/core-utils'
import { pickSearchVideoQuery } from '@server/helpers/query'
import { doJSONRequest } from '@server/helpers/requests'
import { CONFIG } from '@server/initializers/config'
import { WEBSERVER } from '@server/initializers/constants'
@ -7,7 +8,7 @@ import { getOrCreateAPVideo } from '@server/lib/activitypub/videos'
import { Hooks } from '@server/lib/plugins/hooks'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search'
import { HttpStatusCode, ResultList, Video } from '@shared/models'
import { VideosSearchQuery } from '../../../../shared/models/search'
import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search'
import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
@ -46,7 +47,7 @@ export { searchVideosRouter }
// ---------------------------------------------------------------------------
function searchVideos (req: express.Request, res: express.Response) {
const query: VideosSearchQuery = req.query
const query = pickSearchVideoQuery(req.query)
const search = query.search
if (isURISearch(search)) {
@ -60,10 +61,10 @@ function searchVideos (req: express.Request, res: express.Response) {
return searchVideosDB(query, res)
}
async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: express.Response) {
const result = await buildMutedForSearchIndex(res)
let body: VideosSearchQuery = Object.assign(query, result)
let body = { ...query, ...result }
// Use the default instance NSFW policy if not specified
if (!body.nsfw) {
@ -97,13 +98,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
}
}
async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
const apiOptions = await Hooks.wrapObject(Object.assign(query, {
async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) {
const apiOptions = await Hooks.wrapObject({
...query,
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw),
filter: query.filter,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
}), 'filter:api.search.videos.local.list.params')
nsfw: buildNSFWFilter(res, query.nsfw),
user: res.locals.oauth
? res.locals.oauth.token.User
: undefined
}, 'filter:api.search.videos.local.list.params')
const resultList = await Hooks.wrapPromiseFun(
VideoModel.searchAndPopulateAccountAndServer,

View File

@ -1,8 +1,8 @@
import 'multer'
import * as express from 'express'
import { pickCommonVideoQuery } from '@server/helpers/query'
import { sendUndoFollow } from '@server/lib/activitypub/send'
import { VideoChannelModel } from '@server/models/video/video-channel'
import { VideosCommonQuery } from '@shared/models'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
import { getFormattedObjects } from '../../../helpers/utils'
@ -170,20 +170,13 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req)
const query = req.query as VideosCommonQuery
const query = pickCommonVideoQuery(req.query)
const resultList = await VideoModel.listForApi({
start: query.start,
count: query.count,
sort: query.sort,
...query,
includeLocalVideos: false,
categoryOneOf: query.categoryOneOf,
licenceOneOf: query.licenceOneOf,
languageOneOf: query.languageOneOf,
tagsOneOf: query.tagsOneOf,
tagsAllOf: query.tagsAllOf,
nsfw: buildNSFWFilter(res, query.nsfw),
filter: query.filter,
withFiles: false,
followerActorId: user.Account.Actor.id,
user,

View File

@ -1,8 +1,9 @@
import * as express from 'express'
import { pickCommonVideoQuery } from '@server/helpers/query'
import { Hooks } from '@server/lib/plugins/hooks'
import { getServerActor } from '@server/models/application/application'
import { MChannelBannerAccountDefault } from '@server/types/models'
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate, VideosCommonQuery } from '../../../shared'
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
import { resetSequelizeInstance } from '../../helpers/database-utils'
@ -309,20 +310,13 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
const videoChannelInstance = res.locals.videoChannel
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
const countVideos = getCountVideos(req)
const query = req.query as VideosCommonQuery
const query = pickCommonVideoQuery(req.query)
const apiOptions = await Hooks.wrapObject({
...query,
followerActorId,
start: query.start,
count: query.count,
sort: query.sort,
includeLocalVideos: true,
categoryOneOf: query.categoryOneOf,
licenceOneOf: query.licenceOneOf,
languageOneOf: query.languageOneOf,
tagsOneOf: query.tagsOneOf,
tagsAllOf: query.tagsAllOf,
filter: query.filter,
nsfw: buildNSFWFilter(res, query.nsfw),
withFiles: false,
videoChannelId: videoChannelInstance.id,

View File

@ -1,11 +1,11 @@
import * as express from 'express'
import toInt from 'validator/lib/toInt'
import { pickCommonVideoQuery } from '@server/helpers/query'
import { doJSONRequest } from '@server/helpers/requests'
import { LiveManager } from '@server/lib/live'
import { openapiOperationDoc } from '@server/middlewares/doc'
import { getServerActor } from '@server/models/application/application'
import { MVideoAccountLight } from '@server/types/models'
import { VideosCommonQuery } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/models'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
@ -211,22 +211,14 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response
}
async function listVideos (req: express.Request, res: express.Response) {
const query = req.query as VideosCommonQuery
const query = pickCommonVideoQuery(req.query)
const countVideos = getCountVideos(req)
const apiOptions = await Hooks.wrapObject({
start: query.start,
count: query.count,
sort: query.sort,
...query,
includeLocalVideos: true,
categoryOneOf: query.categoryOneOf,
licenceOneOf: query.licenceOneOf,
languageOneOf: query.languageOneOf,
tagsOneOf: query.tagsOneOf,
tagsAllOf: query.tagsAllOf,
nsfw: buildNSFWFilter(res, query.nsfw),
isLive: query.isLive,
filter: query.filter,
withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos

74
server/helpers/query.ts Normal file
View File

@ -0,0 +1,74 @@
import { pick } from '@shared/core-utils'
import {
VideoChannelsSearchQueryAfterSanitize,
VideoPlaylistsSearchQueryAfterSanitize,
VideosCommonQueryAfterSanitize,
VideosSearchQueryAfterSanitize
} from '@shared/models'
function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
return pick(query, [
'start',
'count',
'sort',
'nsfw',
'isLive',
'categoryOneOf',
'licenceOneOf',
'languageOneOf',
'tagsOneOf',
'tagsAllOf',
'filter',
'skipCount'
])
}
function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) {
return {
...pickCommonVideoQuery(query),
...pick(query, [
'searchTarget',
'search',
'host',
'startDate',
'endDate',
'originallyPublishedStartDate',
'originallyPublishedEndDate',
'durationMin',
'durationMax',
'uuids'
])
}
}
function pickSearchChannelQuery (query: VideoChannelsSearchQueryAfterSanitize) {
return pick(query, [
'searchTarget',
'search',
'start',
'count',
'sort',
'host',
'handles'
])
}
function pickSearchPlaylistQuery (query: VideoPlaylistsSearchQueryAfterSanitize) {
return pick(query, [
'searchTarget',
'search',
'start',
'count',
'sort',
'host',
'uuids'
])
}
export {
pickCommonVideoQuery,
pickSearchVideoQuery,
pickSearchPlaylistQuery,
pickSearchChannelQuery
}

View File

@ -1070,7 +1070,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
: undefined
let trendingAlgorithm
let trendingAlgorithm: string
if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
if (options.sort.endsWith('best')) trendingAlgorithm = 'best'

View File

@ -211,6 +211,39 @@ describe('Test videos search', function () {
const search = { ...baseSearch, host: 'framatube.org' }
await check(search, true)
}
{
const goodUUID = '9c9de5e8-0a1e-484a-b099-e80766180a6d'
const goodShortUUID = 'kkGMgK9ZtnKfYAgnEtQxbv'
const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
{
const uuidsMatrix = [
[ goodUUID ],
[ goodUUID, badShortUUID ],
[ badShortUUID, goodShortUUID ],
[ goodUUID, goodShortUUID ]
]
for (const uuids of uuidsMatrix) {
const search = { ...baseSearch, uuids }
await check(search, true)
}
}
{
const uuidsMatrix = [
[ badUUID ],
[ badShortUUID ]
]
for (const uuids of uuidsMatrix) {
const search = { ...baseSearch, uuids }
await check(search, false)
}
}
}
})
it('Should have a correct pagination', async function () {
@ -315,15 +348,6 @@ describe('Test videos search', function () {
describe('Playlists search', async function () {
it('Should make a simple search and not have results', async function () {
const body = await command.searchPlaylists({ search: 'a'.repeat(500) })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
})
it('Should make a search and have results', async function () {
async function check (search: VideoPlaylistsSearchQuery, exists = true) {
const body = await command.advancedPlaylistSearch({ search })
@ -363,9 +387,53 @@ describe('Test videos search', function () {
expect(videoPlaylist.videoChannel.avatar).to.exist
}
it('Should make a simple search and not have results', async function () {
const body = await command.searchPlaylists({ search: 'a'.repeat(500) })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
})
it('Should make a search and have results', async function () {
await check({ search: 'E2E playlist', sort: '-match' }, true)
})
it('Should make host search and have appropriate results', async function () {
await check({ search: 'E2E playlist', host: 'example.com' }, false)
await check({ search: 'E2E playlist', host: 'peertube2.cpy.re' }, true)
await check({ search: 'E2E playlist', host: 'peertube2.cpy.re', sort: '-match' }, true)
})
it('Should make a search by uuids and have appropriate results', async function () {
const goodUUID = '73804a40-da9a-40c2-b1eb-2c6d9eec8f0a'
const goodShortUUID = 'fgei1ws1oa6FCaJ2qZPG29'
const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
{
const uuidsMatrix = [
[ goodUUID ],
[ goodUUID, badShortUUID ],
[ badShortUUID, goodShortUUID ],
[ goodUUID, goodShortUUID ]
]
for (const uuids of uuidsMatrix) {
const search = { search: 'E2E playlist', sort: '-match', uuids }
await check(search, true)
}
}
{
const uuidsMatrix = [
[ badUUID ],
[ badShortUUID ]
]
for (const uuids of uuidsMatrix) {
const search = { search: 'E2E playlist', sort: '-match', uuids }
await check(search, false)
}
}
})
it('Should have a correct pagination', async function () {

View File

@ -1,5 +1,5 @@
function pick <T extends object> (object: T, keys: (keyof T)[]) {
const result: Partial<T> = {}
function pick <O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
const result: any = {}
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(object, key)) {

View File

@ -10,3 +10,9 @@ export interface VideoChannelsSearchQuery extends SearchTargetQuery {
host?: string
handles?: string[]
}
export interface VideoChannelsSearchQueryAfterSanitize extends VideoChannelsSearchQuery {
start: number
count: number
sort: string
}

View File

@ -8,5 +8,13 @@ export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
sort?: string
host?: string
// UUIDs or short UUIDs
uuids?: string[]
}
export interface VideoPlaylistsSearchQueryAfterSanitize extends VideoPlaylistsSearchQuery {
start: number
count: number
sort: string
}

View File

@ -25,6 +25,12 @@ export interface VideosCommonQuery {
skipCount?: boolean
}
export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {
start: number
count: number
sort: string
}
export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
search?: string
}

View File

@ -15,6 +15,12 @@ export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery
durationMin?: number // seconds
durationMax?: number // seconds
// UUIDs or short
// UUIDs or short UUIDs
uuids?: string[]
}
export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
start: number
count: number
sort: string
}