diff options
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r-- | server/models/video/video-channel.ts | 572 |
1 files changed, 271 insertions, 301 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 54f12dce3..9b545a4ef 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,371 +1,341 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' | 2 | import { |
3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 3 | AfterDestroy, |
4 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' | 4 | AllowNull, |
5 | 5 | BelongsTo, | |
6 | import { addMethodsToModel, getSort } from '../utils' | 6 | Column, |
7 | import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' | 7 | CreatedAt, |
8 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | 8 | DataType, |
9 | import { activityPubCollection } from '../../helpers/activitypub' | 9 | Default, |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | ForeignKey, |
11 | 11 | HasMany, | |
12 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> | 12 | Is, |
13 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON | 13 | IsUUID, |
14 | let toActivityPubObject: VideoChannelMethods.ToActivityPubObject | 14 | Model, |
15 | let isOwned: VideoChannelMethods.IsOwned | 15 | Table, |
16 | let countByAccount: VideoChannelMethods.CountByAccount | 16 | UpdatedAt |
17 | let listForApi: VideoChannelMethods.ListForApi | 17 | } from 'sequelize-typescript' |
18 | let listByAccount: VideoChannelMethods.ListByAccount | 18 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' |
19 | let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount | 19 | import { activityPubCollection } from '../../helpers' |
20 | let loadByUUID: VideoChannelMethods.LoadByUUID | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' |
21 | let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | 21 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' |
22 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | 22 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
23 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | 23 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' |
24 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | 24 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send' |
25 | let loadByUrl: VideoChannelMethods.LoadByUrl | 25 | import { AccountModel } from '../account/account' |
26 | let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | 26 | import { ServerModel } from '../server/server' |
27 | 27 | import { getSort, throwIfNotValid } from '../utils' | |
28 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 28 | import { VideoModel } from './video' |
29 | VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', | 29 | import { VideoChannelShareModel } from './video-channel-share' |
30 | |||
31 | @Table({ | ||
32 | tableName: 'videoChannel', | ||
33 | indexes: [ | ||
30 | { | 34 | { |
31 | uuid: { | 35 | fields: [ 'accountId' ] |
32 | type: DataTypes.UUID, | ||
33 | defaultValue: DataTypes.UUIDV4, | ||
34 | allowNull: false, | ||
35 | validate: { | ||
36 | isUUID: 4 | ||
37 | } | ||
38 | }, | ||
39 | name: { | ||
40 | type: DataTypes.STRING, | ||
41 | allowNull: false, | ||
42 | validate: { | ||
43 | nameValid: value => { | ||
44 | const res = isVideoChannelNameValid(value) | ||
45 | if (res === false) throw new Error('Video channel name is not valid.') | ||
46 | } | ||
47 | } | ||
48 | }, | ||
49 | description: { | ||
50 | type: DataTypes.STRING, | ||
51 | allowNull: true, | ||
52 | validate: { | ||
53 | descriptionValid: value => { | ||
54 | const res = isVideoChannelDescriptionValid(value) | ||
55 | if (res === false) throw new Error('Video channel description is not valid.') | ||
56 | } | ||
57 | } | ||
58 | }, | ||
59 | remote: { | ||
60 | type: DataTypes.BOOLEAN, | ||
61 | allowNull: false, | ||
62 | defaultValue: false | ||
63 | }, | ||
64 | url: { | ||
65 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max), | ||
66 | allowNull: false, | ||
67 | validate: { | ||
68 | urlValid: value => { | ||
69 | const res = isActivityPubUrlValid(value) | ||
70 | if (res === false) throw new Error('Video channel URL is not valid.') | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | }, | ||
75 | { | ||
76 | indexes: [ | ||
77 | { | ||
78 | fields: [ 'accountId' ] | ||
79 | } | ||
80 | ], | ||
81 | hooks: { | ||
82 | afterDestroy | ||
83 | } | ||
84 | } | 36 | } |
85 | ) | ||
86 | |||
87 | const classMethods = [ | ||
88 | associate, | ||
89 | |||
90 | listForApi, | ||
91 | listByAccount, | ||
92 | loadByIdAndAccount, | ||
93 | loadAndPopulateAccount, | ||
94 | loadByUUIDAndPopulateAccount, | ||
95 | loadByUUID, | ||
96 | loadByHostAndUUID, | ||
97 | loadAndPopulateAccountAndVideos, | ||
98 | countByAccount, | ||
99 | loadByUrl, | ||
100 | loadByUUIDOrUrl | ||
101 | ] | 37 | ] |
102 | const instanceMethods = [ | 38 | }) |
103 | isOwned, | 39 | export class VideoChannelModel extends Model<VideoChannelModel> { |
104 | toFormattedJSON, | ||
105 | toActivityPubObject | ||
106 | ] | ||
107 | addMethodsToModel(VideoChannel, classMethods, instanceMethods) | ||
108 | 40 | ||
109 | return VideoChannel | 41 | @AllowNull(false) |
110 | } | 42 | @Default(DataType.UUIDV4) |
43 | @IsUUID(4) | ||
44 | @Column(DataType.UUID) | ||
45 | uuid: string | ||
111 | 46 | ||
112 | // ------------------------------ METHODS ------------------------------ | 47 | @AllowNull(false) |
48 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) | ||
49 | @Column | ||
50 | name: string | ||
113 | 51 | ||
114 | isOwned = function (this: VideoChannelInstance) { | 52 | @AllowNull(true) |
115 | return this.remote === false | 53 | @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description')) |
116 | } | 54 | @Column |
117 | 55 | description: string | |
118 | toFormattedJSON = function (this: VideoChannelInstance) { | ||
119 | const json = { | ||
120 | id: this.id, | ||
121 | uuid: this.uuid, | ||
122 | name: this.name, | ||
123 | description: this.description, | ||
124 | isLocal: this.isOwned(), | ||
125 | createdAt: this.createdAt, | ||
126 | updatedAt: this.updatedAt | ||
127 | } | ||
128 | 56 | ||
129 | if (this.Account !== undefined) { | 57 | @AllowNull(false) |
130 | json['owner'] = { | 58 | @Column |
131 | name: this.Account.name, | 59 | remote: boolean |
132 | uuid: this.Account.uuid | ||
133 | } | ||
134 | } | ||
135 | 60 | ||
136 | if (Array.isArray(this.Videos)) { | 61 | @AllowNull(false) |
137 | json['videos'] = this.Videos.map(v => v.toFormattedJSON()) | 62 | @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) |
138 | } | 63 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max)) |
64 | url: string | ||
139 | 65 | ||
140 | return json | 66 | @CreatedAt |
141 | } | 67 | createdAt: Date |
142 | 68 | ||
143 | toActivityPubObject = function (this: VideoChannelInstance) { | 69 | @UpdatedAt |
144 | let sharesObject | 70 | updatedAt: Date |
145 | if (Array.isArray(this.VideoChannelShares)) { | ||
146 | const shares: string[] = [] | ||
147 | 71 | ||
148 | for (const videoChannelShare of this.VideoChannelShares) { | 72 | @ForeignKey(() => AccountModel) |
149 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) | 73 | @Column |
150 | shares.push(shareUrl) | 74 | accountId: number |
151 | } | ||
152 | 75 | ||
153 | sharesObject = activityPubCollection(shares) | 76 | @BelongsTo(() => AccountModel, { |
154 | } | 77 | foreignKey: { |
155 | 78 | allowNull: false | |
156 | const json = { | 79 | }, |
157 | type: 'VideoChannel' as 'VideoChannel', | 80 | onDelete: 'CASCADE' |
158 | id: this.url, | 81 | }) |
159 | uuid: this.uuid, | 82 | Account: AccountModel |
160 | content: this.description, | ||
161 | name: this.name, | ||
162 | published: this.createdAt.toISOString(), | ||
163 | updated: this.updatedAt.toISOString(), | ||
164 | shares: sharesObject | ||
165 | } | ||
166 | |||
167 | return json | ||
168 | } | ||
169 | |||
170 | // ------------------------------ STATICS ------------------------------ | ||
171 | 83 | ||
172 | function associate (models) { | 84 | @HasMany(() => VideoModel, { |
173 | VideoChannel.belongsTo(models.Account, { | ||
174 | foreignKey: { | 85 | foreignKey: { |
175 | name: 'accountId', | 86 | name: 'channelId', |
176 | allowNull: false | 87 | allowNull: false |
177 | }, | 88 | }, |
178 | onDelete: 'CASCADE' | 89 | onDelete: 'CASCADE' |
179 | }) | 90 | }) |
91 | Videos: VideoModel[] | ||
180 | 92 | ||
181 | VideoChannel.hasMany(models.Video, { | 93 | @HasMany(() => VideoChannelShareModel, { |
182 | foreignKey: { | 94 | foreignKey: { |
183 | name: 'channelId', | 95 | name: 'channelId', |
184 | allowNull: false | 96 | allowNull: false |
185 | }, | 97 | }, |
186 | onDelete: 'CASCADE' | 98 | onDelete: 'CASCADE' |
187 | }) | 99 | }) |
188 | } | 100 | VideoChannelShares: VideoChannelShareModel[] |
189 | 101 | ||
190 | function afterDestroy (videoChannel: VideoChannelInstance) { | 102 | @AfterDestroy |
191 | if (videoChannel.isOwned()) { | 103 | static sendDeleteIfOwned (instance: VideoChannelModel) { |
192 | return sendDeleteVideoChannel(videoChannel, undefined) | 104 | if (instance.isOwned()) { |
193 | } | 105 | return sendDeleteVideoChannel(instance, undefined) |
106 | } | ||
194 | 107 | ||
195 | return undefined | 108 | return undefined |
196 | } | 109 | } |
197 | 110 | ||
198 | countByAccount = function (accountId: number) { | 111 | static countByAccount (accountId: number) { |
199 | const query = { | 112 | const query = { |
200 | where: { | 113 | where: { |
201 | accountId | 114 | accountId |
115 | } | ||
202 | } | 116 | } |
117 | |||
118 | return VideoChannelModel.count(query) | ||
203 | } | 119 | } |
204 | 120 | ||
205 | return VideoChannel.count(query) | 121 | static listForApi (start: number, count: number, sort: string) { |
206 | } | 122 | const query = { |
123 | offset: start, | ||
124 | limit: count, | ||
125 | order: [ getSort(sort) ], | ||
126 | include: [ | ||
127 | { | ||
128 | model: AccountModel, | ||
129 | required: true, | ||
130 | include: [ { model: ServerModel, required: false } ] | ||
131 | } | ||
132 | ] | ||
133 | } | ||
207 | 134 | ||
208 | listForApi = function (start: number, count: number, sort: string) { | 135 | return VideoChannelModel.findAndCountAll(query) |
209 | const query = { | 136 | .then(({ rows, count }) => { |
210 | offset: start, | 137 | return { total: count, data: rows } |
211 | limit: count, | 138 | }) |
212 | order: [ getSort(sort) ], | ||
213 | include: [ | ||
214 | { | ||
215 | model: VideoChannel['sequelize'].models.Account, | ||
216 | required: true, | ||
217 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
218 | } | ||
219 | ] | ||
220 | } | 139 | } |
221 | 140 | ||
222 | return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { | 141 | static listByAccount (accountId: number) { |
223 | return { total: count, data: rows } | 142 | const query = { |
224 | }) | 143 | order: [ getSort('createdAt') ], |
225 | } | 144 | include: [ |
145 | { | ||
146 | model: AccountModel, | ||
147 | where: { | ||
148 | id: accountId | ||
149 | }, | ||
150 | required: true, | ||
151 | include: [ { model: ServerModel, required: false } ] | ||
152 | } | ||
153 | ] | ||
154 | } | ||
226 | 155 | ||
227 | listByAccount = function (accountId: number) { | 156 | return VideoChannelModel.findAndCountAll(query) |
228 | const query = { | 157 | .then(({ rows, count }) => { |
229 | order: [ getSort('createdAt') ], | 158 | return { total: count, data: rows } |
230 | include: [ | 159 | }) |
231 | { | ||
232 | model: VideoChannel['sequelize'].models.Account, | ||
233 | where: { | ||
234 | id: accountId | ||
235 | }, | ||
236 | required: true, | ||
237 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
238 | } | ||
239 | ] | ||
240 | } | 160 | } |
241 | 161 | ||
242 | return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { | 162 | static loadByUUID (uuid: string, t?: Sequelize.Transaction) { |
243 | return { total: count, data: rows } | 163 | const query: IFindOptions<VideoChannelModel> = { |
244 | }) | 164 | where: { |
245 | } | 165 | uuid |
166 | } | ||
167 | } | ||
168 | |||
169 | if (t !== undefined) query.transaction = t | ||
246 | 170 | ||
247 | loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { | 171 | return VideoChannelModel.findOne(query) |
248 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 172 | } |
249 | where: { | 173 | |
250 | uuid | 174 | static loadByUrl (url: string, t?: Sequelize.Transaction) { |
175 | const query: IFindOptions<VideoChannelModel> = { | ||
176 | where: { | ||
177 | url | ||
178 | }, | ||
179 | include: [ AccountModel ] | ||
251 | } | 180 | } |
181 | |||
182 | if (t !== undefined) query.transaction = t | ||
183 | |||
184 | return VideoChannelModel.findOne(query) | ||
252 | } | 185 | } |
253 | 186 | ||
254 | if (t !== undefined) query.transaction = t | 187 | static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) { |
188 | const query: IFindOptions<VideoChannelModel> = { | ||
189 | where: { | ||
190 | [ Sequelize.Op.or ]: [ | ||
191 | { uuid }, | ||
192 | { url } | ||
193 | ] | ||
194 | } | ||
195 | } | ||
255 | 196 | ||
256 | return VideoChannel.findOne(query) | 197 | if (t !== undefined) query.transaction = t |
257 | } | ||
258 | 198 | ||
259 | loadByUrl = function (url: string, t?: Sequelize.Transaction) { | 199 | return VideoChannelModel.findOne(query) |
260 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | ||
261 | where: { | ||
262 | url | ||
263 | }, | ||
264 | include: [ VideoChannel['sequelize'].models.Account ] | ||
265 | } | 200 | } |
266 | 201 | ||
267 | if (t !== undefined) query.transaction = t | 202 | static loadByHostAndUUID (fromHost: string, uuid: string, t?: Sequelize.Transaction) { |
203 | const query: IFindOptions<VideoChannelModel> = { | ||
204 | where: { | ||
205 | uuid | ||
206 | }, | ||
207 | include: [ | ||
208 | { | ||
209 | model: AccountModel, | ||
210 | include: [ | ||
211 | { | ||
212 | model: ServerModel, | ||
213 | required: true, | ||
214 | where: { | ||
215 | host: fromHost | ||
216 | } | ||
217 | } | ||
218 | ] | ||
219 | } | ||
220 | ] | ||
221 | } | ||
268 | 222 | ||
269 | return VideoChannel.findOne(query) | 223 | if (t !== undefined) query.transaction = t |
270 | } | 224 | |
225 | return VideoChannelModel.findOne(query) | ||
226 | } | ||
271 | 227 | ||
272 | loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) { | 228 | static loadByIdAndAccount (id: number, accountId: number) { |
273 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 229 | const options = { |
274 | where: { | 230 | where: { |
275 | [Sequelize.Op.or]: [ | 231 | id, |
276 | { uuid }, | 232 | accountId |
277 | { url } | 233 | }, |
234 | include: [ | ||
235 | { | ||
236 | model: AccountModel, | ||
237 | include: [ { model: ServerModel, required: false } ] | ||
238 | } | ||
278 | ] | 239 | ] |
279 | } | 240 | } |
241 | |||
242 | return VideoChannelModel.findOne(options) | ||
280 | } | 243 | } |
281 | 244 | ||
282 | if (t !== undefined) query.transaction = t | 245 | static loadAndPopulateAccount (id: number) { |
246 | const options = { | ||
247 | include: [ | ||
248 | { | ||
249 | model: AccountModel, | ||
250 | include: [ { model: ServerModel, required: false } ] | ||
251 | } | ||
252 | ] | ||
253 | } | ||
283 | 254 | ||
284 | return VideoChannel.findOne(query) | 255 | return VideoChannelModel.findById(id, options) |
285 | } | 256 | } |
286 | 257 | ||
287 | loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { | 258 | static loadByUUIDAndPopulateAccount (uuid: string) { |
288 | const query: Sequelize.FindOptions<VideoChannelAttributes> = { | 259 | const options = { |
289 | where: { | 260 | where: { |
290 | uuid | 261 | uuid |
291 | }, | 262 | }, |
292 | include: [ | 263 | include: [ |
293 | { | 264 | { |
294 | model: VideoChannel['sequelize'].models.Account, | 265 | model: AccountModel, |
295 | include: [ | 266 | include: [ { model: ServerModel, required: false } ] |
296 | { | 267 | } |
297 | model: VideoChannel['sequelize'].models.Server, | 268 | ] |
298 | required: true, | 269 | } |
299 | where: { | 270 | |
300 | host: fromHost | 271 | return VideoChannelModel.findOne(options) |
301 | } | ||
302 | } | ||
303 | ] | ||
304 | } | ||
305 | ] | ||
306 | } | 272 | } |
307 | 273 | ||
308 | if (t !== undefined) query.transaction = t | 274 | static loadAndPopulateAccountAndVideos (id: number) { |
275 | const options = { | ||
276 | include: [ | ||
277 | { | ||
278 | model: AccountModel, | ||
279 | include: [ { model: ServerModel, required: false } ] | ||
280 | }, | ||
281 | VideoModel | ||
282 | ] | ||
283 | } | ||
309 | 284 | ||
310 | return VideoChannel.findOne(query) | 285 | return VideoChannelModel.findById(id, options) |
311 | } | 286 | } |
312 | 287 | ||
313 | loadByIdAndAccount = function (id: number, accountId: number) { | 288 | isOwned () { |
314 | const options = { | 289 | return this.remote === false |
315 | where: { | ||
316 | id, | ||
317 | accountId | ||
318 | }, | ||
319 | include: [ | ||
320 | { | ||
321 | model: VideoChannel['sequelize'].models.Account, | ||
322 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
323 | } | ||
324 | ] | ||
325 | } | 290 | } |
326 | 291 | ||
327 | return VideoChannel.findOne(options) | 292 | toFormattedJSON () { |
328 | } | 293 | const json = { |
294 | id: this.id, | ||
295 | uuid: this.uuid, | ||
296 | name: this.name, | ||
297 | description: this.description, | ||
298 | isLocal: this.isOwned(), | ||
299 | createdAt: this.createdAt, | ||
300 | updatedAt: this.updatedAt | ||
301 | } | ||
329 | 302 | ||
330 | loadAndPopulateAccount = function (id: number) { | 303 | if (this.Account !== undefined) { |
331 | const options = { | 304 | json[ 'owner' ] = { |
332 | include: [ | 305 | name: this.Account.name, |
333 | { | 306 | uuid: this.Account.uuid |
334 | model: VideoChannel['sequelize'].models.Account, | ||
335 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
336 | } | 307 | } |
337 | ] | 308 | } |
309 | |||
310 | if (Array.isArray(this.Videos)) { | ||
311 | json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON()) | ||
312 | } | ||
313 | |||
314 | return json | ||
338 | } | 315 | } |
339 | 316 | ||
340 | return VideoChannel.findById(id, options) | 317 | toActivityPubObject () { |
341 | } | 318 | let sharesObject |
319 | if (Array.isArray(this.VideoChannelShares)) { | ||
320 | const shares: string[] = [] | ||
342 | 321 | ||
343 | loadByUUIDAndPopulateAccount = function (uuid: string) { | 322 | for (const videoChannelShare of this.VideoChannelShares) { |
344 | const options = { | 323 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) |
345 | where: { | 324 | shares.push(shareUrl) |
346 | uuid | ||
347 | }, | ||
348 | include: [ | ||
349 | { | ||
350 | model: VideoChannel['sequelize'].models.Account, | ||
351 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | ||
352 | } | 325 | } |
353 | ] | ||
354 | } | ||
355 | 326 | ||
356 | return VideoChannel.findOne(options) | 327 | sharesObject = activityPubCollection(shares) |
357 | } | 328 | } |
358 | 329 | ||
359 | loadAndPopulateAccountAndVideos = function (id: number) { | 330 | return { |
360 | const options = { | 331 | type: 'VideoChannel' as 'VideoChannel', |
361 | include: [ | 332 | id: this.url, |
362 | { | 333 | uuid: this.uuid, |
363 | model: VideoChannel['sequelize'].models.Account, | 334 | content: this.description, |
364 | include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] | 335 | name: this.name, |
365 | }, | 336 | published: this.createdAt.toISOString(), |
366 | VideoChannel['sequelize'].models.Video | 337 | updated: this.updatedAt.toISOString(), |
367 | ] | 338 | shares: sharesObject |
339 | } | ||
368 | } | 340 | } |
369 | |||
370 | return VideoChannel.findById(id, options) | ||
371 | } | 341 | } |