diff options
author | Chocobozzz <me@florianbigard.com> | 2018-08-23 17:58:39 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-08-27 09:41:54 +0200 |
commit | f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9 (patch) | |
tree | 2050443febcdb2a3eec68b7bbf9687e26dcb24dc /server/models/video/video-channel.ts | |
parent | 240085d0056fd97ac3c7fa8fa4ce9bc32afc4d6e (diff) | |
download | PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.gz PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.zst PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.zip |
Add ability to search video channels
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r-- | server/models/video/video-channel.ts | 163 |
1 files changed, 136 insertions, 27 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 9f80e0b8d..7d717fc68 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | Is, | 12 | Is, |
13 | Model, | 13 | Model, |
14 | Scopes, | 14 | Scopes, |
15 | Sequelize, | ||
15 | Table, | 16 | Table, |
16 | UpdatedAt | 17 | UpdatedAt |
17 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
@@ -24,19 +25,36 @@ import { | |||
24 | } from '../../helpers/custom-validators/video-channels' | 25 | } from '../../helpers/custom-validators/video-channels' |
25 | import { sendDeleteActor } from '../../lib/activitypub/send' | 26 | import { sendDeleteActor } from '../../lib/activitypub/send' |
26 | import { AccountModel } from '../account/account' | 27 | import { AccountModel } from '../account/account' |
27 | import { ActorModel } from '../activitypub/actor' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
28 | import { getSort, throwIfNotValid } from '../utils' | 29 | import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
29 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
30 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 31 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
31 | import { AvatarModel } from '../avatar/avatar' | ||
32 | import { ServerModel } from '../server/server' | 32 | import { ServerModel } from '../server/server' |
33 | import { DefineIndexesOptions } from 'sequelize' | ||
34 | |||
35 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | ||
36 | const indexes: DefineIndexesOptions[] = [ | ||
37 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
38 | |||
39 | { | ||
40 | fields: [ 'accountId' ] | ||
41 | }, | ||
42 | { | ||
43 | fields: [ 'actorId' ] | ||
44 | } | ||
45 | ] | ||
33 | 46 | ||
34 | enum ScopeNames { | 47 | enum ScopeNames { |
48 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | ||
35 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
36 | WITH_ACTOR = 'WITH_ACTOR', | 50 | WITH_ACTOR = 'WITH_ACTOR', |
37 | WITH_VIDEOS = 'WITH_VIDEOS' | 51 | WITH_VIDEOS = 'WITH_VIDEOS' |
38 | } | 52 | } |
39 | 53 | ||
54 | type AvailableForListOptions = { | ||
55 | actorId: number | ||
56 | } | ||
57 | |||
40 | @DefaultScope({ | 58 | @DefaultScope({ |
41 | include: [ | 59 | include: [ |
42 | { | 60 | { |
@@ -46,23 +64,57 @@ enum ScopeNames { | |||
46 | ] | 64 | ] |
47 | }) | 65 | }) |
48 | @Scopes({ | 66 | @Scopes({ |
49 | [ScopeNames.WITH_ACCOUNT]: { | 67 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { |
50 | include: [ | 68 | const actorIdNumber = parseInt(options.actorId + '', 10) |
51 | { | 69 | |
52 | model: () => AccountModel.unscoped(), | 70 | // Only list local channels OR channels that are on an instance followed by actorId |
53 | required: true, | 71 | const inQueryInstanceFollow = '(' + |
54 | include: [ | 72 | 'SELECT "actor"."serverId" FROM "actor" ' + |
55 | { | 73 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = actor.id ' + |
56 | model: () => ActorModel.unscoped(), | 74 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
57 | required: true, | 75 | ')' |
58 | include: [ | 76 | |
77 | return { | ||
78 | include: [ | ||
79 | { | ||
80 | attributes: { | ||
81 | exclude: unusedActorAttributesForAPI | ||
82 | }, | ||
83 | model: ActorModel, | ||
84 | where: { | ||
85 | [Sequelize.Op.or]: [ | ||
86 | { | ||
87 | serverId: null | ||
88 | }, | ||
59 | { | 89 | { |
60 | model: () => AvatarModel.unscoped(), | 90 | serverId: { |
61 | required: false | 91 | [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow) |
92 | } | ||
62 | } | 93 | } |
63 | ] | 94 | ] |
64 | } | 95 | } |
65 | ] | 96 | }, |
97 | { | ||
98 | model: AccountModel, | ||
99 | required: true, | ||
100 | include: [ | ||
101 | { | ||
102 | attributes: { | ||
103 | exclude: unusedActorAttributesForAPI | ||
104 | }, | ||
105 | model: ActorModel, // Default scope includes avatar and server | ||
106 | required: true | ||
107 | } | ||
108 | ] | ||
109 | } | ||
110 | ] | ||
111 | } | ||
112 | }, | ||
113 | [ScopeNames.WITH_ACCOUNT]: { | ||
114 | include: [ | ||
115 | { | ||
116 | model: () => AccountModel, | ||
117 | required: true | ||
66 | } | 118 | } |
67 | ] | 119 | ] |
68 | }, | 120 | }, |
@@ -79,14 +131,7 @@ enum ScopeNames { | |||
79 | }) | 131 | }) |
80 | @Table({ | 132 | @Table({ |
81 | tableName: 'videoChannel', | 133 | tableName: 'videoChannel', |
82 | indexes: [ | 134 | indexes |
83 | { | ||
84 | fields: [ 'accountId' ] | ||
85 | }, | ||
86 | { | ||
87 | fields: [ 'actorId' ] | ||
88 | } | ||
89 | ] | ||
90 | }) | 135 | }) |
91 | export class VideoChannelModel extends Model<VideoChannelModel> { | 136 | export class VideoChannelModel extends Model<VideoChannelModel> { |
92 | 137 | ||
@@ -170,15 +215,61 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
170 | return VideoChannelModel.count(query) | 215 | return VideoChannelModel.count(query) |
171 | } | 216 | } |
172 | 217 | ||
173 | static listForApi (start: number, count: number, sort: string) { | 218 | static listForApi (actorId: number, start: number, count: number, sort: string) { |
174 | const query = { | 219 | const query = { |
175 | offset: start, | 220 | offset: start, |
176 | limit: count, | 221 | limit: count, |
177 | order: getSort(sort) | 222 | order: getSort(sort) |
178 | } | 223 | } |
179 | 224 | ||
225 | const scopes = { | ||
226 | method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ] | ||
227 | } | ||
180 | return VideoChannelModel | 228 | return VideoChannelModel |
181 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 229 | .scope(scopes) |
230 | .findAndCountAll(query) | ||
231 | .then(({ rows, count }) => { | ||
232 | return { total: count, data: rows } | ||
233 | }) | ||
234 | } | ||
235 | |||
236 | static searchForApi (options: { | ||
237 | actorId: number | ||
238 | search: string | ||
239 | start: number | ||
240 | count: number | ||
241 | sort: string | ||
242 | }) { | ||
243 | const attributesInclude = [] | ||
244 | const escapedSearch = VideoModel.sequelize.escape(options.search) | ||
245 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | ||
246 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | ||
247 | |||
248 | const query = { | ||
249 | attributes: { | ||
250 | include: attributesInclude | ||
251 | }, | ||
252 | offset: options.start, | ||
253 | limit: options.count, | ||
254 | order: getSort(options.sort), | ||
255 | where: { | ||
256 | id: { | ||
257 | [ Sequelize.Op.in ]: Sequelize.literal( | ||
258 | '(' + | ||
259 | 'SELECT id FROM "videoChannel" WHERE ' + | ||
260 | 'lower(immutable_unaccent("name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | ||
261 | 'lower(immutable_unaccent("name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | ||
262 | ')' | ||
263 | ) | ||
264 | } | ||
265 | } | ||
266 | } | ||
267 | |||
268 | const scopes = { | ||
269 | method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ] | ||
270 | } | ||
271 | return VideoChannelModel | ||
272 | .scope(scopes) | ||
182 | .findAndCountAll(query) | 273 | .findAndCountAll(query) |
183 | .then(({ rows, count }) => { | 274 | .then(({ rows, count }) => { |
184 | return { total: count, data: rows } | 275 | return { total: count, data: rows } |
@@ -239,7 +330,25 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
239 | } | 330 | } |
240 | 331 | ||
241 | return VideoChannelModel | 332 | return VideoChannelModel |
242 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 333 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
334 | .findOne(query) | ||
335 | } | ||
336 | |||
337 | static loadByUrlAndPopulateAccount (url: string) { | ||
338 | const query = { | ||
339 | include: [ | ||
340 | { | ||
341 | model: ActorModel, | ||
342 | required: true, | ||
343 | where: { | ||
344 | url | ||
345 | } | ||
346 | } | ||
347 | ] | ||
348 | } | ||
349 | |||
350 | return VideoChannelModel | ||
351 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
243 | .findOne(query) | 352 | .findOne(query) |
244 | } | 353 | } |
245 | 354 | ||