]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/sql/comment/video-comment-list-query-builder.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / models / video / sql / comment / video-comment-list-query-builder.ts
CommitLineData
cde3d90d
C
1import { Model, Sequelize, Transaction } from 'sequelize'
2import { AbstractRunQuery, ModelBuilder } from '@server/models/shared'
cde3d90d 3import { ActorImageType, VideoPrivacy } from '@shared/models'
eba2a7a8 4import { createSafeIn, getSort, parseRowCountResult } from '../../../shared'
cde3d90d
C
5import { VideoCommentTableAttributes } from './video-comment-table-attributes'
6
7export interface ListVideoCommentsOptions {
8 selectType: 'api' | 'feed' | 'comment-only'
9
10 start?: number
11 count?: number
12 sort?: string
13
14 videoId?: number
15 threadId?: number
16 accountId?: number
17 videoChannelId?: number
18
19 blockerAccountIds?: number[]
20
21 isThread?: boolean
22 notDeleted?: boolean
23 isLocal?: boolean
24 onLocalVideo?: boolean
25 onPublicVideo?: boolean
26 videoAccountOwnerId?: boolean
27
28 search?: string
29 searchAccount?: string
30 searchVideo?: string
31
32 includeReplyCounters?: boolean
33
34 transaction?: Transaction
35}
36
37export class VideoCommentListQueryBuilder extends AbstractRunQuery {
38 private readonly tableAttributes = new VideoCommentTableAttributes()
39
40 private innerQuery: string
41
42 private select = ''
43 private joins = ''
44
45 private innerSelect = ''
46 private innerJoins = ''
0b96a0fb 47 private innerLateralJoins = ''
cde3d90d
C
48 private innerWhere = ''
49
50 private readonly built = {
51 cte: false,
52 accountJoin: false,
53 videoJoin: false,
54 videoChannelJoin: false,
55 avatarJoin: false
56 }
57
58 constructor (
59 protected readonly sequelize: Sequelize,
60 private readonly options: ListVideoCommentsOptions
61 ) {
62 super(sequelize)
0b96a0fb
C
63
64 if (this.options.includeReplyCounters && !this.options.videoId) {
65 throw new Error('Cannot include reply counters without videoId')
66 }
cde3d90d
C
67 }
68
69 async listComments <T extends Model> () {
70 this.buildListQuery()
71
72 const results = await this.runQuery({ nest: true, transaction: this.options.transaction })
73 const modelBuilder = new ModelBuilder<T>(this.sequelize)
74
75 return modelBuilder.createModels(results, 'VideoComment')
76 }
77
78 async countComments () {
79 this.buildCountQuery()
80
81 const result = await this.runQuery({ transaction: this.options.transaction })
82
83 return parseRowCountResult(result)
84 }
85
86 // ---------------------------------------------------------------------------
87
88 private buildListQuery () {
89 this.buildInnerListQuery()
90 this.buildListSelect()
91
92 this.query = `${this.select} ` +
93 `FROM (${this.innerQuery}) AS "VideoCommentModel" ` +
94 `${this.joins} ` +
a8749f7c 95 `${this.getOrder()}`
cde3d90d
C
96 }
97
98 private buildInnerListQuery () {
99 this.buildWhere()
100 this.buildInnerListSelect()
101
102 this.innerQuery = `${this.innerSelect} ` +
103 `FROM "videoComment" AS "VideoCommentModel" ` +
104 `${this.innerJoins} ` +
0b96a0fb 105 `${this.innerLateralJoins} ` +
cde3d90d
C
106 `${this.innerWhere} ` +
107 `${this.getOrder()} ` +
108 `${this.getInnerLimit()}`
109 }
110
111 // ---------------------------------------------------------------------------
112
113 private buildCountQuery () {
114 this.buildWhere()
115
116 this.query = `SELECT COUNT(*) AS "total" ` +
117 `FROM "videoComment" AS "VideoCommentModel" ` +
118 `${this.innerJoins} ` +
119 `${this.innerWhere}`
120 }
121
122 // ---------------------------------------------------------------------------
123
124 private buildWhere () {
125 let where: string[] = []
126
127 if (this.options.videoId) {
128 this.replacements.videoId = this.options.videoId
129
130 where.push('"VideoCommentModel"."videoId" = :videoId')
131 }
132
133 if (this.options.threadId) {
134 this.replacements.threadId = this.options.threadId
135
136 where.push('("VideoCommentModel"."id" = :threadId OR "VideoCommentModel"."originCommentId" = :threadId)')
137 }
138
139 if (this.options.accountId) {
140 this.replacements.accountId = this.options.accountId
141
142 where.push('"VideoCommentModel"."accountId" = :accountId')
143 }
144
145 if (this.options.videoChannelId) {
146 this.buildVideoChannelJoin()
147
148 this.replacements.videoChannelId = this.options.videoChannelId
149
150 where.push('"Account->VideoChannel"."id" = :videoChannelId')
151 }
152
153 if (this.options.blockerAccountIds) {
154 this.buildVideoChannelJoin()
155
156 where = where.concat(this.getBlockWhere('VideoCommentModel', 'Video->VideoChannel'))
157 }
158
159 if (this.options.isThread === true) {
160 where.push('"VideoCommentModel"."inReplyToCommentId" IS NULL')
161 }
162
163 if (this.options.notDeleted === true) {
164 where.push('"VideoCommentModel"."deletedAt" IS NULL')
165 }
166
167 if (this.options.isLocal === true) {
168 this.buildAccountJoin()
169
170 where.push('"Account->Actor"."serverId" IS NULL')
171 } else if (this.options.isLocal === false) {
172 this.buildAccountJoin()
173
174 where.push('"Account->Actor"."serverId" IS NOT NULL')
175 }
176
177 if (this.options.onLocalVideo === true) {
178 this.buildVideoJoin()
179
180 where.push('"Video"."remote" IS FALSE')
181 } else if (this.options.onLocalVideo === false) {
182 this.buildVideoJoin()
183
184 where.push('"Video"."remote" IS TRUE')
185 }
186
187 if (this.options.onPublicVideo === true) {
188 this.buildVideoJoin()
189
190 where.push(`"Video"."privacy" = ${VideoPrivacy.PUBLIC}`)
191 }
192
193 if (this.options.videoAccountOwnerId) {
194 this.buildVideoChannelJoin()
195
196 this.replacements.videoAccountOwnerId = this.options.videoAccountOwnerId
197
198 where.push(`"Video->VideoChannel"."accountId" = :videoAccountOwnerId`)
199 }
200
201 if (this.options.search) {
202 this.buildVideoJoin()
203 this.buildAccountJoin()
204
205 const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%')
206
207 where.push(
208 `(` +
209 `"VideoCommentModel"."text" ILIKE ${escapedLikeSearch} OR ` +
210 `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` +
211 `"Account"."name" ILIKE ${escapedLikeSearch} OR ` +
212 `"Video"."name" ILIKE ${escapedLikeSearch} ` +
213 `)`
214 )
215 }
216
217 if (this.options.searchAccount) {
218 this.buildAccountJoin()
219
220 const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchAccount + '%')
221
222 where.push(
223 `(` +
224 `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` +
225 `"Account"."name" ILIKE ${escapedLikeSearch} ` +
226 `)`
227 )
228 }
229
230 if (this.options.searchVideo) {
231 this.buildVideoJoin()
232
233 const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchVideo + '%')
234
235 where.push(`"Video"."name" ILIKE ${escapedLikeSearch}`)
236 }
237
238 if (where.length !== 0) {
239 this.innerWhere = `WHERE ${where.join(' AND ')}`
240 }
241 }
242
243 private buildAccountJoin () {
244 if (this.built.accountJoin) return
245
246 this.innerJoins += ' LEFT JOIN "account" "Account" ON "Account"."id" = "VideoCommentModel"."accountId" ' +
247 'LEFT JOIN "actor" "Account->Actor" ON "Account->Actor"."id" = "Account"."actorId" ' +
248 'LEFT JOIN "server" "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" '
249
250 this.built.accountJoin = true
251 }
252
253 private buildVideoJoin () {
254 if (this.built.videoJoin) return
255
256 this.innerJoins += ' LEFT JOIN "video" "Video" ON "Video"."id" = "VideoCommentModel"."videoId" '
257
258 this.built.videoJoin = true
259 }
260
261 private buildVideoChannelJoin () {
262 if (this.built.videoChannelJoin) return
263
264 this.buildVideoJoin()
265
266 this.innerJoins += ' LEFT JOIN "videoChannel" "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" '
267
268 this.built.videoChannelJoin = true
269 }
270
271 private buildAvatarsJoin () {
272 if (this.options.selectType !== 'api' && this.options.selectType !== 'feed') return ''
273 if (this.built.avatarJoin) return
274
275 this.joins += `LEFT JOIN "actorImage" "Account->Actor->Avatars" ` +
276 `ON "VideoCommentModel"."Account.Actor.id" = "Account->Actor->Avatars"."actorId" ` +
277 `AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
278
279 this.built.avatarJoin = true
280 }
281
282 // ---------------------------------------------------------------------------
283
284 private buildListSelect () {
285 const toSelect = [ '"VideoCommentModel".*' ]
286
287 if (this.options.selectType === 'api' || this.options.selectType === 'feed') {
288 this.buildAvatarsJoin()
289
290 toSelect.push(this.tableAttributes.getAvatarAttributes())
291 }
292
cde3d90d
C
293 this.select = this.buildSelect(toSelect)
294 }
295
296 private buildInnerListSelect () {
297 let toSelect = [ this.tableAttributes.getVideoCommentAttributes() ]
298
299 if (this.options.selectType === 'api' || this.options.selectType === 'feed') {
300 this.buildAccountJoin()
301 this.buildVideoJoin()
302
303 toSelect = toSelect.concat([
304 this.tableAttributes.getVideoAttributes(),
305 this.tableAttributes.getAccountAttributes(),
306 this.tableAttributes.getActorAttributes(),
307 this.tableAttributes.getServerAttributes()
308 ])
309 }
310
0b96a0fb
C
311 if (this.options.includeReplyCounters === true) {
312 this.buildTotalRepliesSelect()
313 this.buildAuthorTotalRepliesSelect()
314
315 toSelect.push('"totalRepliesFromVideoAuthor"."count" AS "totalRepliesFromVideoAuthor"')
316 toSelect.push('"totalReplies"."count" AS "totalReplies"')
317 }
318
cde3d90d
C
319 this.innerSelect = this.buildSelect(toSelect)
320 }
321
322 // ---------------------------------------------------------------------------
323
324 private getBlockWhere (commentTableName: string, channelTableName: string) {
325 const where: string[] = []
326
327 const blockerIdsString = createSafeIn(
328 this.sequelize,
329 this.options.blockerAccountIds,
330 [ `"${channelTableName}"."accountId"` ]
331 )
332
333 where.push(
334 `NOT EXISTS (` +
335 `SELECT 1 FROM "accountBlocklist" ` +
336 `WHERE "targetAccountId" = "${commentTableName}"."accountId" ` +
337 `AND "accountId" IN (${blockerIdsString})` +
338 `)`
339 )
340
341 where.push(
342 `NOT EXISTS (` +
343 `SELECT 1 FROM "account" ` +
344 `INNER JOIN "actor" ON account."actorId" = actor.id ` +
345 `INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` +
346 `WHERE "account"."id" = "${commentTableName}"."accountId" ` +
347 `AND "serverBlocklist"."accountId" IN (${blockerIdsString})` +
348 `)`
349 )
350
351 return where
352 }
353
354 // ---------------------------------------------------------------------------
355
0b96a0fb 356 private buildTotalRepliesSelect () {
cde3d90d
C
357 const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ')
358
0b96a0fb
C
359 // Help the planner by providing videoId that should filter out many comments
360 this.replacements.videoId = this.options.videoId
361
362 this.innerLateralJoins += `LEFT JOIN LATERAL (` +
363 `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` +
364 `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` +
cde3d90d
C
365 `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` +
366 `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` +
367 `AND "deletedAt" IS NULL ` +
368 `AND ${blockWhereString} ` +
0b96a0fb 369 `) "totalReplies" ON TRUE `
cde3d90d
C
370 }
371
0b96a0fb
C
372 private buildAuthorTotalRepliesSelect () {
373 // Help the planner by providing videoId that should filter out many comments
374 this.replacements.videoId = this.options.videoId
375
376 this.innerLateralJoins += `LEFT JOIN LATERAL (` +
377 `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` +
378 `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` +
cde3d90d
C
379 `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` +
380 `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` +
0b96a0fb 381 `) "totalRepliesFromVideoAuthor" ON TRUE `
cde3d90d
C
382 }
383
384 private getOrder () {
385 if (!this.options.sort) return ''
386
eba2a7a8 387 const orders = getSort(this.options.sort)
cde3d90d
C
388
389 return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ')
390 }
391
cde3d90d
C
392 private getInnerLimit () {
393 if (!this.options.count) return ''
394
395 this.replacements.limit = this.options.count
396 this.replacements.offset = this.options.start || 0
397
398 return `LIMIT :limit OFFSET :offset `
399 }
400}