aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/sql/video/index.ts3
-rw-r--r--server/models/video/sql/video/shared/abstract-run-query.ts (renamed from server/models/video/sql/shared/abstract-run-query.ts)0
-rw-r--r--server/models/video/sql/video/shared/abstract-video-query-builder.ts (renamed from server/models/video/sql/shared/abstract-video-query-builder.ts)15
-rw-r--r--server/models/video/sql/video/shared/video-file-query-builder.ts (renamed from server/models/video/sql/shared/video-file-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/shared/video-model-builder.ts (renamed from server/models/video/sql/shared/video-model-builder.ts)51
-rw-r--r--server/models/video/sql/video/shared/video-table-attributes.ts (renamed from server/models/video/sql/shared/video-table-attributes.ts)4
-rw-r--r--server/models/video/sql/video/video-model-get-query-builder.ts (renamed from server/models/video/sql/video-model-get-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts (renamed from server/models/video/sql/videos-id-list-query-builder.ts)0
-rw-r--r--server/models/video/sql/video/videos-model-list-query-builder.ts (renamed from server/models/video/sql/videos-model-list-query-builder.ts)0
-rw-r--r--server/models/video/video-channel.ts209
-rw-r--r--server/models/video/video-comment.ts102
-rw-r--r--server/models/video/video-import.ts11
-rw-r--r--server/models/video/video-playlist-element.ts36
-rw-r--r--server/models/video/video-playlist.ts98
-rw-r--r--server/models/video/video-share.ts5
-rw-r--r--server/models/video/video.ts18
16 files changed, 341 insertions, 211 deletions
diff --git a/server/models/video/sql/video/index.ts b/server/models/video/sql/video/index.ts
new file mode 100644
index 000000000..e9132d5e1
--- /dev/null
+++ b/server/models/video/sql/video/index.ts
@@ -0,0 +1,3 @@
1export * from './video-model-get-query-builder'
2export * from './videos-id-list-query-builder'
3export * from './videos-model-list-query-builder'
diff --git a/server/models/video/sql/shared/abstract-run-query.ts b/server/models/video/sql/video/shared/abstract-run-query.ts
index 8e7a7642d..8e7a7642d 100644
--- a/server/models/video/sql/shared/abstract-run-query.ts
+++ b/server/models/video/sql/video/shared/abstract-run-query.ts
diff --git a/server/models/video/sql/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
index a6afb04e4..490e5e6e0 100644
--- a/server/models/video/sql/shared/abstract-video-query-builder.ts
+++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
@@ -1,5 +1,6 @@
1import { createSafeIn } from '@server/models/utils' 1import { createSafeIn } from '@server/models/utils'
2import { MUserAccountId } from '@server/types/models' 2import { MUserAccountId } from '@server/types/models'
3import { ActorImageType } from '@shared/models'
3import validator from 'validator' 4import validator from 'validator'
4import { AbstractRunQuery } from './abstract-run-query' 5import { AbstractRunQuery } from './abstract-run-query'
5import { VideoTableAttributes } from './video-table-attributes' 6import { VideoTableAttributes } from './video-table-attributes'
@@ -42,8 +43,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
42 ) 43 )
43 44
44 this.addJoin( 45 this.addJoin(
45 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + 46 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatars" ' +
46 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' 47 'ON "VideoChannel->Actor"."id" = "VideoChannel->Actor->Avatars"."actorId" ' +
48 `AND "VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
47 ) 49 )
48 50
49 this.attributes = { 51 this.attributes = {
@@ -51,7 +53,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
51 53
52 ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), 54 ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()),
53 ...this.buildActorInclude('VideoChannel->Actor'), 55 ...this.buildActorInclude('VideoChannel->Actor'),
54 ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), 56 ...this.buildAvatarInclude('VideoChannel->Actor->Avatars'),
55 ...this.buildServerInclude('VideoChannel->Actor->Server') 57 ...this.buildServerInclude('VideoChannel->Actor->Server')
56 } 58 }
57 } 59 }
@@ -68,8 +70,9 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
68 ) 70 )
69 71
70 this.addJoin( 72 this.addJoin(
71 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + 73 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatars" ' +
72 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' 74 'ON "VideoChannel->Account"."actorId"= "VideoChannel->Account->Actor->Avatars"."actorId" ' +
75 `AND "VideoChannel->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
73 ) 76 )
74 77
75 this.attributes = { 78 this.attributes = {
@@ -77,7 +80,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
77 80
78 ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), 81 ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()),
79 ...this.buildActorInclude('VideoChannel->Account->Actor'), 82 ...this.buildActorInclude('VideoChannel->Account->Actor'),
80 ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), 83 ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatars'),
81 ...this.buildServerInclude('VideoChannel->Account->Actor->Server') 84 ...this.buildServerInclude('VideoChannel->Account->Actor->Server')
82 } 85 }
83 } 86 }
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/video/shared/video-file-query-builder.ts
index 3eb3dc07d..3eb3dc07d 100644
--- a/server/models/video/sql/shared/video-file-query-builder.ts
+++ b/server/models/video/sql/video/shared/video-file-query-builder.ts
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/video/shared/video-model-builder.ts
index 7751d8e68..b1b47b721 100644
--- a/server/models/video/sql/shared/video-model-builder.ts
+++ b/server/models/video/sql/video/shared/video-model-builder.ts
@@ -9,15 +9,15 @@ import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
9import { TrackerModel } from '@server/models/server/tracker' 9import { TrackerModel } from '@server/models/server/tracker'
10import { UserVideoHistoryModel } from '@server/models/user/user-video-history' 10import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
11import { VideoInclude } from '@shared/models' 11import { VideoInclude } from '@shared/models'
12import { ScheduleVideoUpdateModel } from '../../schedule-video-update' 12import { ScheduleVideoUpdateModel } from '../../../schedule-video-update'
13import { TagModel } from '../../tag' 13import { TagModel } from '../../../tag'
14import { ThumbnailModel } from '../../thumbnail' 14import { ThumbnailModel } from '../../../thumbnail'
15import { VideoModel } from '../../video' 15import { VideoModel } from '../../../video'
16import { VideoBlacklistModel } from '../../video-blacklist' 16import { VideoBlacklistModel } from '../../../video-blacklist'
17import { VideoChannelModel } from '../../video-channel' 17import { VideoChannelModel } from '../../../video-channel'
18import { VideoFileModel } from '../../video-file' 18import { VideoFileModel } from '../../../video-file'
19import { VideoLiveModel } from '../../video-live' 19import { VideoLiveModel } from '../../../video-live'
20import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' 20import { VideoStreamingPlaylistModel } from '../../../video-streaming-playlist'
21import { VideoTableAttributes } from './video-table-attributes' 21import { VideoTableAttributes } from './video-table-attributes'
22 22
23type SQLRow = { [id: string]: string | number } 23type SQLRow = { [id: string]: string | number }
@@ -34,6 +34,7 @@ export class VideoModelBuilder {
34 private videoFileMemo: { [ id: number ]: VideoFileModel } 34 private videoFileMemo: { [ id: number ]: VideoFileModel }
35 35
36 private thumbnailsDone: Set<any> 36 private thumbnailsDone: Set<any>
37 private actorImagesDone: Set<any>
37 private historyDone: Set<any> 38 private historyDone: Set<any>
38 private blacklistDone: Set<any> 39 private blacklistDone: Set<any>
39 private accountBlocklistDone: Set<any> 40 private accountBlocklistDone: Set<any>
@@ -69,11 +70,21 @@ export class VideoModelBuilder {
69 for (const row of rows) { 70 for (const row of rows) {
70 this.buildVideoAndAccount(row) 71 this.buildVideoAndAccount(row)
71 72
72 const videoModel = this.videosMemo[row.id] 73 const videoModel = this.videosMemo[row.id as number]
73 74
74 this.setUserHistory(row, videoModel) 75 this.setUserHistory(row, videoModel)
75 this.addThumbnail(row, videoModel) 76 this.addThumbnail(row, videoModel)
76 77
78 const channelActor = videoModel.VideoChannel?.Actor
79 if (channelActor) {
80 this.addActorAvatar(row, 'VideoChannel.Actor', channelActor)
81 }
82
83 const accountActor = videoModel.VideoChannel?.Account?.Actor
84 if (accountActor) {
85 this.addActorAvatar(row, 'VideoChannel.Account.Actor', accountActor)
86 }
87
77 if (!rowsWebTorrentFiles) { 88 if (!rowsWebTorrentFiles) {
78 this.addWebTorrentFile(row, videoModel) 89 this.addWebTorrentFile(row, videoModel)
79 } 90 }
@@ -113,6 +124,7 @@ export class VideoModelBuilder {
113 this.videoFileMemo = {} 124 this.videoFileMemo = {}
114 125
115 this.thumbnailsDone = new Set() 126 this.thumbnailsDone = new Set()
127 this.actorImagesDone = new Set()
116 this.historyDone = new Set() 128 this.historyDone = new Set()
117 this.blacklistDone = new Set() 129 this.blacklistDone = new Set()
118 this.liveDone = new Set() 130 this.liveDone = new Set()
@@ -195,13 +207,8 @@ export class VideoModelBuilder {
195 207
196 private buildActor (row: SQLRow, prefix: string) { 208 private buildActor (row: SQLRow, prefix: string) {
197 const actorPrefix = `${prefix}.Actor` 209 const actorPrefix = `${prefix}.Actor`
198 const avatarPrefix = `${actorPrefix}.Avatar`
199 const serverPrefix = `${actorPrefix}.Server` 210 const serverPrefix = `${actorPrefix}.Server`
200 211
201 const avatarModel = row[`${avatarPrefix}.id`] !== null
202 ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts)
203 : null
204
205 const serverModel = row[`${serverPrefix}.id`] !== null 212 const serverModel = row[`${serverPrefix}.id`] !== null
206 ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) 213 ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts)
207 : null 214 : null
@@ -209,8 +216,8 @@ export class VideoModelBuilder {
209 if (serverModel) serverModel.BlockedBy = [] 216 if (serverModel) serverModel.BlockedBy = []
210 217
211 const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) 218 const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts)
212 actorModel.Avatar = avatarModel
213 actorModel.Server = serverModel 219 actorModel.Server = serverModel
220 actorModel.Avatars = []
214 221
215 return actorModel 222 return actorModel
216 } 223 }
@@ -226,6 +233,18 @@ export class VideoModelBuilder {
226 this.historyDone.add(id) 233 this.historyDone.add(id)
227 } 234 }
228 235
236 private addActorAvatar (row: SQLRow, actorPrefix: string, actor: ActorModel) {
237 const avatarPrefix = `${actorPrefix}.Avatar`
238 const id = row[`${avatarPrefix}.id`]
239 if (!id || this.actorImagesDone.has(id)) return
240
241 const attributes = this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix)
242 const avatarModel = new ActorImageModel(attributes, this.buildOpts)
243 actor.Avatars.push(avatarModel)
244
245 this.actorImagesDone.add(id)
246 }
247
229 private addThumbnail (row: SQLRow, videoModel: VideoModel) { 248 private addThumbnail (row: SQLRow, videoModel: VideoModel) {
230 const id = row['Thumbnails.id'] 249 const id = row['Thumbnails.id']
231 if (!id || this.thumbnailsDone.has(id)) return 250 if (!id || this.thumbnailsDone.has(id)) return
diff --git a/server/models/video/sql/shared/video-table-attributes.ts b/server/models/video/sql/video/shared/video-table-attributes.ts
index 8a8d2073a..df2ed3fb0 100644
--- a/server/models/video/sql/shared/video-table-attributes.ts
+++ b/server/models/video/sql/video/shared/video-table-attributes.ts
@@ -186,8 +186,7 @@ export class VideoTableAttributes {
186 'id', 186 'id',
187 'preferredUsername', 187 'preferredUsername',
188 'url', 188 'url',
189 'serverId', 189 'serverId'
190 'avatarId'
191 ] 190 ]
192 191
193 if (this.mode === 'get') { 192 if (this.mode === 'get') {
@@ -212,6 +211,7 @@ export class VideoTableAttributes {
212 getAvatarAttributes () { 211 getAvatarAttributes () {
213 let attributeKeys = [ 212 let attributeKeys = [
214 'id', 213 'id',
214 'width',
215 'filename', 215 'filename',
216 'type', 216 'type',
217 'fileUrl', 217 'fileUrl',
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video/video-model-get-query-builder.ts
index a65c96097..a65c96097 100644
--- a/server/models/video/sql/video-model-get-query-builder.ts
+++ b/server/models/video/sql/video/video-model-get-query-builder.ts
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts
index 098e15359..098e15359 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/video/videos-id-list-query-builder.ts
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/video/videos-model-list-query-builder.ts
index b15b29ec3..b15b29ec3 100644
--- a/server/models/video/sql/videos-model-list-query-builder.ts
+++ b/server/models/video/sql/video/videos-model-list-query-builder.ts
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 2c6669bcb..410fd6d3f 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -31,6 +31,7 @@ import {
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { sendDeleteActor } from '../../lib/activitypub/send' 32import { sendDeleteActor } from '../../lib/activitypub/send'
33import { 33import {
34 MChannel,
34 MChannelActor, 35 MChannelActor,
35 MChannelAP, 36 MChannelAP,
36 MChannelBannerAccountDefault, 37 MChannelBannerAccountDefault,
@@ -62,6 +63,7 @@ type AvailableForListOptions = {
62 search?: string 63 search?: string
63 host?: string 64 host?: string
64 handles?: string[] 65 handles?: string[]
66 forCount?: boolean
65} 67}
66 68
67type AvailableWithStatsOptions = { 69type AvailableWithStatsOptions = {
@@ -116,70 +118,91 @@ export type SummaryOptions = {
116 }) 118 })
117 } 119 }
118 120
119 let rootWhere: WhereOptions 121 if (Array.isArray(options.handles) && options.handles.length !== 0) {
120 if (options.handles) { 122 const or: string[] = []
121 const or: WhereOptions[] = []
122 123
123 for (const handle of options.handles || []) { 124 for (const handle of options.handles || []) {
124 const [ preferredUsername, host ] = handle.split('@') 125 const [ preferredUsername, host ] = handle.split('@')
125 126
126 if (!host || host === WEBSERVER.HOST) { 127 if (!host || host === WEBSERVER.HOST) {
127 or.push({ 128 or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`)
128 '$Actor.preferredUsername$': preferredUsername,
129 '$Actor.serverId$': null
130 })
131 } else { 129 } else {
132 or.push({ 130 or.push(
133 '$Actor.preferredUsername$': preferredUsername, 131 `(` +
134 '$Actor.Server.host$': host 132 `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` +
135 }) 133 `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` +
134 `)`
135 )
136 } 136 }
137 } 137 }
138 138
139 rootWhere = { 139 whereActorAnd.push({
140 [Op.or]: or 140 id: {
141 } 141 [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`)
142 }
143 })
144 }
145
146 const channelInclude: Includeable[] = []
147 const accountInclude: Includeable[] = []
148
149 if (options.forCount !== true) {
150 accountInclude.push({
151 model: ServerModel,
152 required: false
153 })
154
155 accountInclude.push({
156 model: ActorImageModel,
157 as: 'Avatars',
158 required: false
159 })
160
161 channelInclude.push({
162 model: ActorImageModel,
163 as: 'Avatars',
164 required: false
165 })
166
167 channelInclude.push({
168 model: ActorImageModel,
169 as: 'Banners',
170 required: false
171 })
172 }
173
174 if (options.forCount !== true || serverRequired) {
175 channelInclude.push({
176 model: ServerModel,
177 duplicating: false,
178 required: serverRequired,
179 where: whereServer
180 })
142 } 181 }
143 182
144 return { 183 return {
145 where: rootWhere,
146 include: [ 184 include: [
147 { 185 {
148 attributes: { 186 attributes: {
149 exclude: unusedActorAttributesForAPI 187 exclude: unusedActorAttributesForAPI
150 }, 188 },
151 model: ActorModel, 189 model: ActorModel.unscoped(),
152 where: { 190 where: {
153 [Op.and]: whereActorAnd 191 [Op.and]: whereActorAnd
154 }, 192 },
155 include: [ 193 include: channelInclude
156 {
157 model: ServerModel,
158 required: serverRequired,
159 where: whereServer
160 },
161 {
162 model: ActorImageModel,
163 as: 'Avatar',
164 required: false
165 },
166 {
167 model: ActorImageModel,
168 as: 'Banner',
169 required: false
170 }
171 ]
172 }, 194 },
173 { 195 {
174 model: AccountModel, 196 model: AccountModel.unscoped(),
175 required: true, 197 required: true,
176 include: [ 198 include: [
177 { 199 {
178 attributes: { 200 attributes: {
179 exclude: unusedActorAttributesForAPI 201 exclude: unusedActorAttributesForAPI
180 }, 202 },
181 model: ActorModel, // Default scope includes avatar and server 203 model: ActorModel.unscoped(),
182 required: true 204 required: true,
205 include: accountInclude
183 } 206 }
184 ] 207 ]
185 } 208 }
@@ -189,7 +212,7 @@ export type SummaryOptions = {
189 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { 212 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
190 const include: Includeable[] = [ 213 const include: Includeable[] = [
191 { 214 {
192 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 215 attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
193 model: ActorModel.unscoped(), 216 model: ActorModel.unscoped(),
194 required: options.actorRequired ?? true, 217 required: options.actorRequired ?? true,
195 include: [ 218 include: [
@@ -199,8 +222,8 @@ export type SummaryOptions = {
199 required: false 222 required: false
200 }, 223 },
201 { 224 {
202 model: ActorImageModel.unscoped(), 225 model: ActorImageModel,
203 as: 'Avatar', 226 as: 'Avatars',
204 required: false 227 required: false
205 } 228 }
206 ] 229 ]
@@ -245,7 +268,7 @@ export type SummaryOptions = {
245 { 268 {
246 model: ActorImageModel, 269 model: ActorImageModel,
247 required: false, 270 required: false,
248 as: 'Banner' 271 as: 'Banners'
249 } 272 }
250 ] 273 ]
251 } 274 }
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
474 order: getSort(parameters.sort) 497 order: getSort(parameters.sort)
475 } 498 }
476 499
477 return VideoChannelModel 500 const getScope = (forCount: boolean) => {
478 .scope({ 501 return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] }
479 method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] 502 }
480 }) 503
481 .findAndCountAll(query) 504 return Promise.all([
482 .then(({ rows, count }) => { 505 VideoChannelModel.scope(getScope(true)).count(),
483 return { total: count, data: rows } 506 VideoChannelModel.scope(getScope(false)).findAll(query)
484 }) 507 ]).then(([ total, data ]) => ({ total, data }))
485 } 508 }
486 509
487 static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { 510 static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
519 where 542 where
520 } 543 }
521 544
522 return VideoChannelModel 545 const getScope = (forCount: boolean) => {
523 .scope({ 546 return {
524 method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] 547 method: [
525 }) 548 ScopeNames.FOR_API, {
526 .findAndCountAll(query) 549 ...pick(options, [ 'actorId', 'host', 'handles' ]),
527 .then(({ rows, count }) => { 550
528 return { total: count, data: rows } 551 forCount
529 }) 552 } as AvailableForListOptions
553 ]
554 }
555 }
556
557 return Promise.all([
558 VideoChannelModel.scope(getScope(true)).count(query),
559 VideoChannelModel.scope(getScope(false)).findAll(query)
560 ]).then(([ total, data ]) => ({ total, data }))
530 } 561 }
531 562
532 static listByAccountForAPI (options: { 563 static listByAccountForAPI (options: {
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
552 } 583 }
553 : null 584 : null
554 585
555 const query = { 586 const getQuery = (forCount: boolean) => {
556 offset: options.start, 587 const accountModel = forCount
557 limit: options.count, 588 ? AccountModel.unscoped()
558 order: getSort(options.sort), 589 : AccountModel
559 include: [ 590
560 { 591 return {
561 model: AccountModel, 592 offset: options.start,
562 where: { 593 limit: options.count,
563 id: options.accountId 594 order: getSort(options.sort),
564 }, 595 include: [
565 required: true 596 {
566 } 597 model: accountModel,
567 ], 598 where: {
568 where 599 id: options.accountId
600 },
601 required: true
602 }
603 ],
604 where
605 }
569 } 606 }
570 607
571 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] 608 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
576 }) 613 })
577 } 614 }
578 615
579 return VideoChannelModel 616 return Promise.all([
580 .scope(scopes) 617 VideoChannelModel.scope(scopes).count(getQuery(true)),
581 .findAndCountAll(query) 618 VideoChannelModel.scope(scopes).findAll(getQuery(false))
582 .then(({ rows, count }) => { 619 ]).then(([ total, data ]) => ({ total, data }))
583 return { total: count, data: rows }
584 })
585 } 620 }
586 621
587 static listAllByAccount (accountId: number) { 622 static listAllByAccount (accountId: number): Promise<MChannel[]> {
588 const query = { 623 const query = {
589 limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, 624 limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER,
590 include: [ 625 include: [
591 { 626 {
592 attributes: [], 627 attributes: [],
593 model: AccountModel, 628 model: AccountModel.unscoped(),
594 where: { 629 where: {
595 id: accountId 630 id: accountId
596 }, 631 },
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
621 { 656 {
622 model: ActorImageModel, 657 model: ActorImageModel,
623 required: false, 658 required: false,
624 as: 'Banner' 659 as: 'Banners'
625 } 660 }
626 ] 661 ]
627 } 662 }
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
655 { 690 {
656 model: ActorImageModel, 691 model: ActorImageModel,
657 required: false, 692 required: false,
658 as: 'Banner' 693 as: 'Banners'
659 } 694 }
660 ] 695 ]
661 } 696 }
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
685 { 720 {
686 model: ActorImageModel, 721 model: ActorImageModel,
687 required: false, 722 required: false,
688 as: 'Banner' 723 as: 'Banners'
689 } 724 }
690 ] 725 ]
691 } 726 }
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
706 displayName: this.getDisplayName(), 741 displayName: this.getDisplayName(),
707 url: actor.url, 742 url: actor.url,
708 host: actor.host, 743 host: actor.host,
744 avatars: actor.avatars,
745
746 // TODO: remove, deprecated in 4.2
709 avatar: actor.avatar 747 avatar: actor.avatar
710 } 748 }
711 } 749 }
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
736 support: this.support, 774 support: this.support,
737 isLocal: this.Actor.isOwned(), 775 isLocal: this.Actor.isOwned(),
738 updatedAt: this.updatedAt, 776 updatedAt: this.updatedAt,
777
739 ownerAccount: undefined, 778 ownerAccount: undefined,
779
740 videosCount, 780 videosCount,
741 viewsPerDay 781 viewsPerDay,
782
783 avatars: actor.avatars,
784
785 // TODO: remove, deprecated in 4.2
786 avatar: actor.avatar
742 } 787 }
743 788
744 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() 789 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index fa77455bc..2d60c6a30 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,5 +1,5 @@
1import { uniq } from 'lodash' 1import { uniq } from 'lodash'
2import { FindAndCountOptions, FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 2import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
4 AllowNull, 4 AllowNull,
5 BelongsTo, 5 BelongsTo,
@@ -16,8 +16,8 @@ import {
16} from 'sequelize-typescript' 16} from 'sequelize-typescript'
17import { getServerActor } from '@server/models/application/application' 17import { getServerActor } from '@server/models/application/application'
18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
19import { AttributesOnly } from '@shared/typescript-utils'
20import { VideoPrivacy } from '@shared/models' 19import { VideoPrivacy } from '@shared/models'
20import { AttributesOnly } from '@shared/typescript-utils'
21import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 21import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
22import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 22import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
23import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' 23import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model'
@@ -363,40 +363,43 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
363 Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) 363 Object.assign(whereVideo, searchAttribute(searchVideo, 'name'))
364 } 364 }
365 365
366 const query: FindAndCountOptions = { 366 const getQuery = (forCount: boolean) => {
367 offset: start, 367 return {
368 limit: count, 368 offset: start,
369 order: getCommentSort(sort), 369 limit: count,
370 where, 370 order: getCommentSort(sort),
371 include: [ 371 where,
372 { 372 include: [
373 model: AccountModel.unscoped(), 373 {
374 required: true, 374 model: AccountModel.unscoped(),
375 where: whereAccount, 375 required: true,
376 include: [ 376 where: whereAccount,
377 { 377 include: [
378 attributes: { 378 {
379 exclude: unusedActorAttributesForAPI 379 attributes: {
380 }, 380 exclude: unusedActorAttributesForAPI
381 model: ActorModel, // Default scope includes avatar and server 381 },
382 required: true, 382 model: forCount === true
383 where: whereActor 383 ? ActorModel.unscoped() // Default scope includes avatar and server
384 } 384 : ActorModel,
385 ] 385 required: true,
386 }, 386 where: whereActor
387 { 387 }
388 model: VideoModel.unscoped(), 388 ]
389 required: true, 389 },
390 where: whereVideo 390 {
391 } 391 model: VideoModel.unscoped(),
392 ] 392 required: true,
393 where: whereVideo
394 }
395 ]
396 }
393 } 397 }
394 398
395 return VideoCommentModel 399 return Promise.all([
396 .findAndCountAll(query) 400 VideoCommentModel.count(getQuery(true)),
397 .then(({ rows, count }) => { 401 VideoCommentModel.findAll(getQuery(false))
398 return { total: count, data: rows } 402 ]).then(([ total, data ]) => ({ total, data }))
399 })
400 } 403 }
401 404
402 static async listThreadsForApi (parameters: { 405 static async listThreadsForApi (parameters: {
@@ -443,14 +446,20 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
443 } 446 }
444 } 447 }
445 448
446 const scopesList: (string | ScopeOptions)[] = [ 449 const findScopesList: (string | ScopeOptions)[] = [
447 ScopeNames.WITH_ACCOUNT_FOR_API, 450 ScopeNames.WITH_ACCOUNT_FOR_API,
448 { 451 {
449 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] 452 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
450 } 453 }
451 ] 454 ]
452 455
453 const queryCount = { 456 const countScopesList: ScopeOptions[] = [
457 {
458 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
459 }
460 ]
461
462 const notDeletedQueryCount = {
454 where: { 463 where: {
455 videoId, 464 videoId,
456 deletedAt: null, 465 deletedAt: null,
@@ -459,9 +468,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
459 } 468 }
460 469
461 return Promise.all([ 470 return Promise.all([
462 VideoCommentModel.scope(scopesList).findAndCountAll(queryList), 471 VideoCommentModel.scope(findScopesList).findAll(queryList),
463 VideoCommentModel.count(queryCount) 472 VideoCommentModel.scope(countScopesList).count(queryList),
464 ]).then(([ { rows, count }, totalNotDeletedComments ]) => { 473 VideoCommentModel.count(notDeletedQueryCount)
474 ]).then(([ rows, count, totalNotDeletedComments ]) => {
465 return { total: count, data: rows, totalNotDeletedComments } 475 return { total: count, data: rows, totalNotDeletedComments }
466 }) 476 })
467 } 477 }
@@ -512,11 +522,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
512 } 522 }
513 ] 523 ]
514 524
515 return VideoCommentModel.scope(scopes) 525 return Promise.all([
516 .findAndCountAll(query) 526 VideoCommentModel.count(query),
517 .then(({ rows, count }) => { 527 VideoCommentModel.scope(scopes).findAll(query)
518 return { total: count, data: rows } 528 ]).then(([ total, data ]) => ({ total, data }))
519 })
520 } 529 }
521 530
522 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { 531 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
@@ -565,7 +574,10 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
565 transaction: t 574 transaction: t
566 } 575 }
567 576
568 return VideoCommentModel.findAndCountAll<MComment>(query) 577 return Promise.all([
578 VideoCommentModel.count(query),
579 VideoCommentModel.findAll<MComment>(query)
580 ]).then(([ total, data ]) => ({ total, data }))
569 } 581 }
570 582
571 static async listForFeed (parameters: { 583 static async listForFeed (parameters: {
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index 5d2b230e8..1d8296060 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -155,13 +155,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
155 where 155 where
156 } 156 }
157 157
158 return VideoImportModel.findAndCountAll<MVideoImportDefault>(query) 158 return Promise.all([
159 .then(({ rows, count }) => { 159 VideoImportModel.unscoped().count(query),
160 return { 160 VideoImportModel.findAll<MVideoImportDefault>(query)
161 data: rows, 161 ]).then(([ total, data ]) => ({ total, data }))
162 total: count
163 }
164 })
165 } 162 }
166 163
167 getTargetIdentifier () { 164 getTargetIdentifier () {
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index e20e32f8b..4e4160818 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -23,6 +23,7 @@ import {
23 MVideoPlaylistElementVideoUrlPlaylistPrivacy, 23 MVideoPlaylistElementVideoUrlPlaylistPrivacy,
24 MVideoPlaylistVideoThumbnail 24 MVideoPlaylistVideoThumbnail
25} from '@server/types/models/video/video-playlist-element' 25} from '@server/types/models/video/video-playlist-element'
26import { AttributesOnly } from '@shared/typescript-utils'
26import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 27import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
27import { VideoPrivacy } from '../../../shared/models/videos' 28import { VideoPrivacy } from '../../../shared/models/videos'
28import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' 29import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
@@ -32,7 +33,6 @@ import { AccountModel } from '../account/account'
32import { getSort, throwIfNotValid } from '../utils' 33import { getSort, throwIfNotValid } from '../utils'
33import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' 34import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
34import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
35import { AttributesOnly } from '@shared/typescript-utils'
36 36
37@Table({ 37@Table({
38 tableName: 'videoPlaylistElement', 38 tableName: 'videoPlaylistElement',
@@ -208,22 +208,28 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
208 } 208 }
209 209
210 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) { 210 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
211 const query = { 211 const getQuery = (forCount: boolean) => {
212 attributes: [ 'url' ], 212 return {
213 offset: start, 213 attributes: forCount
214 limit: count, 214 ? []
215 order: getSort('position'), 215 : [ 'url' ],
216 where: { 216 offset: start,
217 videoPlaylistId 217 limit: count,
218 }, 218 order: getSort('position'),
219 transaction: t 219 where: {
220 videoPlaylistId
221 },
222 transaction: t
223 }
220 } 224 }
221 225
222 return VideoPlaylistElementModel 226 return Promise.all([
223 .findAndCountAll(query) 227 VideoPlaylistElementModel.count(getQuery(true)),
224 .then(({ rows, count }) => { 228 VideoPlaylistElementModel.findAll(getQuery(false))
225 return { total: count, data: rows.map(e => e.url) } 229 ]).then(([ total, rows ]) => ({
226 }) 230 total,
231 data: rows.map(e => e.url)
232 }))
227 } 233 }
228 234
229 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> { 235 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Promise<MVideoPlaylistVideoThumbnail> {
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index c125db3ff..ae5e237ec 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -1,5 +1,5 @@
1import { join } from 'path' 1import { join } from 'path'
2import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 2import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
4 AllowNull, 4 AllowNull,
5 BelongsTo, 5 BelongsTo,
@@ -86,6 +86,7 @@ type AvailableForListOptions = {
86 host?: string 86 host?: string
87 uuids?: string[] 87 uuids?: string[]
88 withVideos?: boolean 88 withVideos?: boolean
89 forCount?: boolean
89} 90}
90 91
91function getVideoLengthSelect () { 92function getVideoLengthSelect () {
@@ -239,23 +240,28 @@ function getVideoLengthSelect () {
239 [Op.and]: whereAnd 240 [Op.and]: whereAnd
240 } 241 }
241 242
243 const include: Includeable[] = [
244 {
245 model: AccountModel.scope({
246 method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ]
247 }),
248 required: true
249 }
250 ]
251
252 if (options.forCount !== true) {
253 include.push({
254 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
255 required: false
256 })
257 }
258
242 return { 259 return {
243 attributes: { 260 attributes: {
244 include: attributesInclude 261 include: attributesInclude
245 }, 262 },
246 where, 263 where,
247 include: [ 264 include
248 {
249 model: AccountModel.scope({
250 method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ]
251 }),
252 required: true
253 },
254 {
255 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
256 required: false
257 }
258 ]
259 } as FindOptions 265 } as FindOptions
260 } 266 }
261})) 267}))
@@ -369,12 +375,23 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
369 order: getPlaylistSort(options.sort) 375 order: getPlaylistSort(options.sort)
370 } 376 }
371 377
372 const scopes: (string | ScopeOptions)[] = [ 378 const commonAvailableForListOptions = pick(options, [
379 'type',
380 'followerActorId',
381 'accountId',
382 'videoChannelId',
383 'listMyPlaylists',
384 'search',
385 'host',
386 'uuids'
387 ])
388
389 const scopesFind: (string | ScopeOptions)[] = [
373 { 390 {
374 method: [ 391 method: [
375 ScopeNames.AVAILABLE_FOR_LIST, 392 ScopeNames.AVAILABLE_FOR_LIST,
376 { 393 {
377 ...pick(options, [ 'type', 'followerActorId', 'accountId', 'videoChannelId', 'listMyPlaylists', 'search', 'host', 'uuids' ]), 394 ...commonAvailableForListOptions,
378 395
379 withVideos: options.withVideos || false 396 withVideos: options.withVideos || false
380 } as AvailableForListOptions 397 } as AvailableForListOptions
@@ -384,12 +401,26 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
384 ScopeNames.WITH_THUMBNAIL 401 ScopeNames.WITH_THUMBNAIL
385 ] 402 ]
386 403
387 return VideoPlaylistModel 404 const scopesCount: (string | ScopeOptions)[] = [
388 .scope(scopes) 405 {
389 .findAndCountAll(query) 406 method: [
390 .then(({ rows, count }) => { 407 ScopeNames.AVAILABLE_FOR_LIST,
391 return { total: count, data: rows } 408
392 }) 409 {
410 ...commonAvailableForListOptions,
411
412 withVideos: options.withVideos || false,
413 forCount: true
414 } as AvailableForListOptions
415 ]
416 },
417 ScopeNames.WITH_VIDEOS_LENGTH
418 ]
419
420 return Promise.all([
421 VideoPlaylistModel.scope(scopesCount).count(),
422 VideoPlaylistModel.scope(scopesFind).findAll(query)
423 ]).then(([ count, rows ]) => ({ total: count, data: rows }))
393 } 424 }
394 425
395 static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & { 426 static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search'| 'host'| 'uuids'> & {
@@ -419,17 +450,24 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
419 Object.assign(where, { videoChannelId: options.channel.id }) 450 Object.assign(where, { videoChannelId: options.channel.id })
420 } 451 }
421 452
422 const query = { 453 const getQuery = (forCount: boolean) => {
423 attributes: [ 'url' ], 454 return {
424 offset: start, 455 attributes: forCount === true
425 limit: count, 456 ? []
426 where 457 : [ 'url' ],
458 offset: start,
459 limit: count,
460 where
461 }
427 } 462 }
428 463
429 return VideoPlaylistModel.findAndCountAll(query) 464 return Promise.all([
430 .then(({ rows, count }) => { 465 VideoPlaylistModel.count(getQuery(true)),
431 return { total: count, data: rows.map(p => p.url) } 466 VideoPlaylistModel.findAll(getQuery(false))
432 }) 467 ]).then(([ total, rows ]) => ({
468 total,
469 data: rows.map(p => p.url)
470 }))
433 } 471 }
434 472
435 static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { 473 static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> {
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index f6659b992..ad95dec6e 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -183,7 +183,10 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode
183 transaction: t 183 transaction: t
184 } 184 }
185 185
186 return VideoShareModel.findAndCountAll(query) 186 return Promise.all([
187 VideoShareModel.count(query),
188 VideoShareModel.findAll(query)
189 ]).then(([ total, data ]) => ({ total, data }))
187 } 190 }
188 191
189 static listRemoteShareUrlsOfLocalVideos () { 192 static listRemoteShareUrlsOfLocalVideos () {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 9111c71b0..5536334eb 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -114,9 +114,13 @@ import {
114 videoModelToFormattedJSON 114 videoModelToFormattedJSON
115} from './formatter/video-format-utils' 115} from './formatter/video-format-utils'
116import { ScheduleVideoUpdateModel } from './schedule-video-update' 116import { ScheduleVideoUpdateModel } from './schedule-video-update'
117import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder' 117import {
118import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' 118 BuildVideosListQueryOptions,
119import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' 119 DisplayOnlyForFollowerOptions,
120 VideoModelGetQueryBuilder,
121 VideosIdListQueryBuilder,
122 VideosModelListQueryBuilder
123} from './sql/video'
120import { TagModel } from './tag' 124import { TagModel } from './tag'
121import { ThumbnailModel } from './thumbnail' 125import { ThumbnailModel } from './thumbnail'
122import { VideoBlacklistModel } from './video-blacklist' 126import { VideoBlacklistModel } from './video-blacklist'
@@ -229,8 +233,8 @@ export type ForAPIOptions = {
229 required: false 233 required: false
230 }, 234 },
231 { 235 {
232 model: ActorImageModel.unscoped(), 236 model: ActorImageModel,
233 as: 'Avatar', 237 as: 'Avatars',
234 required: false 238 required: false
235 } 239 }
236 ] 240 ]
@@ -252,8 +256,8 @@ export type ForAPIOptions = {
252 required: false 256 required: false
253 }, 257 },
254 { 258 {
255 model: ActorImageModel.unscoped(), 259 model: ActorImageModel,
256 as: 'Avatar', 260 as: 'Avatars',
257 required: false 261 required: false
258 } 262 }
259 ] 263 ]