Refractor and optimize AP collections
[oweals/peertube.git] / server / models / video / video-channel.ts
1 import {
2   AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Is, Model, Scopes, Table,
3   UpdatedAt, Default, DataType
4 } from 'sequelize-typescript'
5 import { ActivityPubActor } from '../../../shared/models/activitypub'
6 import { VideoChannel } from '../../../shared/models/videos'
7 import {
8   isVideoChannelDescriptionValid, isVideoChannelNameValid,
9   isVideoChannelSupportValid
10 } from '../../helpers/custom-validators/video-channels'
11 import { logger } from '../../helpers/logger'
12 import { sendDeleteActor } from '../../lib/activitypub/send'
13 import { AccountModel } from '../account/account'
14 import { ActorModel } from '../activitypub/actor'
15 import { getSort, throwIfNotValid } from '../utils'
16 import { VideoModel } from './video'
17 import { CONSTRAINTS_FIELDS } from '../../initializers'
18
19 enum ScopeNames {
20   WITH_ACCOUNT = 'WITH_ACCOUNT',
21   WITH_ACTOR = 'WITH_ACTOR',
22   WITH_VIDEOS = 'WITH_VIDEOS'
23 }
24
25 @DefaultScope({
26   include: [
27     {
28       model: () => ActorModel,
29       required: true
30     }
31   ]
32 })
33 @Scopes({
34   [ScopeNames.WITH_ACCOUNT]: {
35     include: [
36       {
37         model: () => AccountModel.unscoped(),
38         required: true,
39         include: [
40           {
41             model: () => ActorModel.unscoped(),
42             required: true
43           }
44         ]
45       }
46     ]
47   },
48   [ScopeNames.WITH_VIDEOS]: {
49     include: [
50       () => VideoModel
51     ]
52   },
53   [ScopeNames.WITH_ACTOR]: {
54     include: [
55       () => ActorModel
56     ]
57   }
58 })
59 @Table({
60   tableName: 'videoChannel',
61   indexes: [
62     {
63       fields: [ 'accountId' ]
64     }
65   ]
66 })
67 export class VideoChannelModel extends Model<VideoChannelModel> {
68
69   @AllowNull(false)
70   @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
71   @Column
72   name: string
73
74   @AllowNull(true)
75   @Default(null)
76   @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description'))
77   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
78   description: string
79
80   @AllowNull(true)
81   @Default(null)
82   @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support'))
83   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
84   support: string
85
86   @CreatedAt
87   createdAt: Date
88
89   @UpdatedAt
90   updatedAt: Date
91
92   @ForeignKey(() => ActorModel)
93   @Column
94   actorId: number
95
96   @BelongsTo(() => ActorModel, {
97     foreignKey: {
98       allowNull: false
99     },
100     onDelete: 'cascade'
101   })
102   Actor: ActorModel
103
104   @ForeignKey(() => AccountModel)
105   @Column
106   accountId: number
107
108   @BelongsTo(() => AccountModel, {
109     foreignKey: {
110       allowNull: false
111     },
112     hooks: true
113   })
114   Account: AccountModel
115
116   @HasMany(() => VideoModel, {
117     foreignKey: {
118       name: 'channelId',
119       allowNull: false
120     },
121     onDelete: 'CASCADE',
122     hooks: true
123   })
124   Videos: VideoModel[]
125
126   @BeforeDestroy
127   static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
128     if (!instance.Actor) {
129       instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel
130     }
131
132     if (instance.Actor.isOwned()) {
133       logger.debug('Sending delete of actor of video channel %s.', instance.Actor.url)
134
135       return sendDeleteActor(instance.Actor, options.transaction)
136     }
137
138     return undefined
139   }
140
141   static countByAccount (accountId: number) {
142     const query = {
143       where: {
144         accountId
145       }
146     }
147
148     return VideoChannelModel.count(query)
149   }
150
151   static listForApi (start: number, count: number, sort: string) {
152     const query = {
153       offset: start,
154       limit: count,
155       order: getSort(sort)
156     }
157
158     return VideoChannelModel
159       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
160       .findAndCountAll(query)
161       .then(({ rows, count }) => {
162         return { total: count, data: rows }
163       })
164   }
165
166   static listByAccount (accountId: number) {
167     const query = {
168       order: getSort('createdAt'),
169       include: [
170         {
171           model: AccountModel,
172           where: {
173             id: accountId
174           },
175           required: true
176         }
177       ]
178     }
179
180     return VideoChannelModel
181       .findAndCountAll(query)
182       .then(({ rows, count }) => {
183         return { total: count, data: rows }
184       })
185   }
186
187   static loadByIdAndAccount (id: number, accountId: number) {
188     const options = {
189       where: {
190         id,
191         accountId
192       }
193     }
194
195     return VideoChannelModel
196       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
197       .findOne(options)
198   }
199
200   static loadAndPopulateAccount (id: number) {
201     return VideoChannelModel
202       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
203       .findById(id)
204   }
205
206   static loadByUUIDAndPopulateAccount (uuid: string) {
207     const options = {
208       include: [
209         {
210           model: ActorModel,
211           required: true,
212           where: {
213             uuid
214           }
215         }
216       ]
217     }
218
219     return VideoChannelModel
220       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
221       .findOne(options)
222   }
223
224   static loadAndPopulateAccountAndVideos (id: number) {
225     const options = {
226       include: [
227         VideoModel
228       ]
229     }
230
231     return VideoChannelModel
232       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
233       .findById(id, options)
234   }
235
236   toFormattedJSON (): VideoChannel {
237     const actor = this.Actor.toFormattedJSON()
238     const videoChannel = {
239       id: this.id,
240       displayName: this.name,
241       description: this.description,
242       support: this.support,
243       isLocal: this.Actor.isOwned(),
244       createdAt: this.createdAt,
245       updatedAt: this.updatedAt,
246       ownerAccount: undefined,
247       videos: undefined
248     }
249
250     if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
251
252     return Object.assign(actor, videoChannel)
253   }
254
255   toActivityPubObject (): ActivityPubActor {
256     const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
257
258     return Object.assign(obj, {
259       summary: this.description,
260       support: this.support,
261       attributedTo: [
262         {
263           type: 'Person' as 'Person',
264           id: this.Account.Actor.url
265         }
266       ]
267     })
268   }
269 }