]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/utils.ts
Fix video job error when video has been deleted
[github/Chocobozzz/PeerTube.git] / server / models / utils.ts
CommitLineData
16c016e8 1import { literal, Op, OrderItem, Sequelize } from 'sequelize'
49cff3a4 2import validator from 'validator'
57c36b27 3
95153292 4type SortType = { sortModel: string, sortValue: string }
06215f15 5
9a629c6e 6// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
1735c825
C
7function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
8 const { direction, field } = buildDirectionAndField(value)
9
9d8ef212 10 let finalField: string | ReturnType<typeof Sequelize.col>
4b54f136
C
11
12 if (field.toLowerCase() === 'match') { // Search
1735c825
C
13 finalField = Sequelize.col('similarity')
14 } else {
15 finalField = field
4b54f136 16 }
5c39adb7 17
1735c825 18 return [ [ finalField, direction ], lastSort ]
9a629c6e
C
19}
20
87a0cac6
C
21function getAdminUsersSort (value: string): OrderItem[] {
22 const { direction, field } = buildDirectionAndField(value)
23
24 let finalField: string | ReturnType<typeof Sequelize.col>
25
26 if (field === 'videoQuotaUsed') { // Users list
27 finalField = Sequelize.col('videoQuotaUsed')
28 } else {
29 finalField = field
30 }
31
32 const nullPolicy = direction === 'ASC'
33 ? 'NULLS FIRST'
34 : 'NULLS LAST'
35
36 // FIXME: typings
37 return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
38}
39
49cff3a4
C
40function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
41 const { direction, field } = buildDirectionAndField(value)
42
43 if (field.toLowerCase() === 'name') {
44 return [ [ 'displayName', direction ], lastSort ]
45 }
46
47 return getSort(value, lastSort)
48}
49
c1125bca
RK
50function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
51 const { direction, field } = buildDirectionAndField(value)
52
53 if (field === 'totalReplies') {
54 return [
55 [ Sequelize.literal('"totalReplies"'), direction ],
56 lastSort
57 ]
58 }
59
60 return getSort(value, lastSort)
61}
62
1735c825
C
63function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
64 const { direction, field } = buildDirectionAndField(value)
5c39adb7 65
1735c825 66 if (field.toLowerCase() === 'trending') { // Sort by aggregation
9a629c6e
C
67 return [
68 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
69
70 [ Sequelize.col('VideoModel.views'), direction ],
71
8e286cdc
RK
72 lastSort
73 ]
74 } else if (field === 'publishedAt') {
75 return [
76 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
77
78 [ Sequelize.col('VideoModel.publishedAt'), direction ],
79
9a629c6e
C
80 lastSort
81 ]
82 }
83
9d8ef212 84 let finalField: string | ReturnType<typeof Sequelize.col>
1735c825
C
85
86 // Alias
87 if (field.toLowerCase() === 'match') { // Search
88 finalField = Sequelize.col('similarity')
89 } else {
90 finalField = field
91 }
92
0c691a18
C
93 const firstSort: OrderItem = typeof finalField === 'string'
94 ? finalField.split('.').concat([ direction ]) as OrderItem
1735c825 95 : [ finalField, direction ]
afa4374a
C
96
97 return [ firstSort, lastSort ]
5c39adb7
C
98}
99
95153292 100function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
1735c825 101 const [ firstSort ] = getSort(value)
792dbaf0 102
0c691a18 103 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[]
3bb6c526 104 return [ firstSort, lastSort ]
792dbaf0
GS
105}
106
bae61627 107function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
cb5ce4cb
C
108 const { direction, field } = buildDirectionAndField(value)
109
110 if (field === 'redundancyAllowed') {
111 return [
bae61627 112 [ 'ActorFollowing.Server.redundancyAllowed', direction ],
cb5ce4cb
C
113 lastSort
114 ]
115 }
116
117 return getSort(value, lastSort)
118}
119
2a491182
F
120function getChannelSyncSort (value: string): OrderItem[] {
121 const { direction, field } = buildDirectionAndField(value)
122 if (field.toLowerCase() === 'videochannel') {
123 return [
124 [ literal('"VideoChannel.name"'), direction ]
125 ]
126 }
127 return [ [ field, direction ] ]
128}
129
9f79ade6 130function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
304a84d5 131 if (!model.createdAt || !model.updatedAt) {
7a4fd56c 132 throw new Error('Miss createdAt & updatedAt attributes to model')
304a84d5
C
133 }
134
9f79ade6
C
135 const now = Date.now()
136 const createdAtTime = model.createdAt.getTime()
137 const updatedAtTime = model.updatedAt.getTime()
138
139 return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
140}
141
1735c825
C
142function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
143 if (nullable && (value === null || value === undefined)) return
144
3fd3ab2d
C
145 if (validator(value) === false) {
146 throw new Error(`"${value}" is not a valid ${fieldName}.`)
147 }
148}
149
57c36b27
C
150function buildTrigramSearchIndex (indexName: string, attribute: string) {
151 return {
152 name: indexName,
b49f22d8
C
153 // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function
154 fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ],
57c36b27
C
155 using: 'gin',
156 operator: 'gin_trgm_ops'
157 }
158}
159
160function createSimilarityAttribute (col: string, value: string) {
161 return Sequelize.fn(
162 'similarity',
163
164 searchTrigramNormalizeCol(col),
165
166 searchTrigramNormalizeValue(value)
167 )
168}
169
696d83fd 170function buildBlockedAccountSQL (blockerIds: number[]) {
7ad9b984
C
171 const blockerIdsString = blockerIds.join(', ')
172
418d092a 173 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
9d6b9d10 174 ' UNION ' +
7ad9b984
C
175 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
176 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
177 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
418d092a
C
178}
179
1c58423f
C
180function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) {
181 const blockerIdsString = blockerIds.join(', ')
182
183 return [
184 literal(
185 `NOT EXISTS (` +
186 ` SELECT 1 FROM "accountBlocklist" ` +
187 ` WHERE "targetAccountId" = ${columnNameJoin} ` +
188 ` AND "accountId" IN (${blockerIdsString})` +
189 `)`
190 ),
191
192 literal(
193 `NOT EXISTS (` +
194 ` SELECT 1 FROM "account" ` +
195 ` INNER JOIN "actor" ON account."actorId" = actor.id ` +
196 ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` +
197 ` WHERE "account"."id" = ${columnNameJoin} ` +
198 ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` +
199 `)`
200 )
201 ]
202}
203
418d092a
C
204function buildServerIdsFollowedBy (actorId: any) {
205 const actorIdNumber = parseInt(actorId + '', 10)
206
207 return '(' +
208 'SELECT "actor"."serverId" FROM "actorFollow" ' +
209 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
210 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
d0800f76 211 ')'
418d092a 212}
7ad9b984 213
418d092a
C
214function buildWhereIdOrUUID (id: number | string) {
215 return validator.isInt('' + id) ? { id } : { uuid: id }
7ad9b984
C
216}
217
3acc5084
C
218function parseAggregateResult (result: any) {
219 if (!result) return 0
220
221 const total = parseInt(result + '', 10)
222 if (isNaN(total)) return 0
223
224 return total
225}
226
bae61627
C
227function parseRowCountResult (result: any) {
228 if (result.length !== 0) return result[0].total
229
230 return 0
231}
232
16c016e8 233function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
5f3e2425
C
234 return stringArr.map(t => {
235 return t === null
236 ? null
16c016e8 237 : sequelize.escape('' + t)
5f3e2425 238 }).join(', ')
3caf77d3
C
239}
240
6b9c966f
C
241function buildLocalAccountIdsIn () {
242 return literal(
243 '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
244 )
245}
246
247function buildLocalActorIdsIn () {
248 return literal(
249 '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
250 )
251}
252
5f3e2425
C
253function buildDirectionAndField (value: string) {
254 let field: string
255 let direction: 'ASC' | 'DESC'
256
257 if (value.substring(0, 1) === '-') {
258 direction = 'DESC'
259 field = value.substring(1)
260 } else {
261 direction = 'ASC'
262 field = value
263 }
264
265 return { direction, field }
266}
267
feb34f6b
C
268function searchAttribute (sourceField?: string, targetField?: string) {
269 if (!sourceField) return {}
0d3a2982
RK
270
271 return {
feb34f6b 272 [targetField]: {
c0d2eac3
C
273 // FIXME: ts error
274 [Op.iLike as any]: `%${sourceField}%`
feb34f6b 275 }
0d3a2982
RK
276 }
277}
278
5c39adb7
C
279// ---------------------------------------------------------------------------
280
65fcc311 281export {
7ad9b984 282 buildBlockedAccountSQL,
1c58423f 283 buildBlockedAccountSQLOptimized,
6b9c966f 284 buildLocalActorIdsIn,
49cff3a4 285 getPlaylistSort,
06215f15 286 SortType,
6b9c966f 287 buildLocalAccountIdsIn,
792dbaf0 288 getSort,
c1125bca 289 getCommentSort,
87a0cac6 290 getAdminUsersSort,
9a629c6e 291 getVideoSort,
95153292 292 getBlacklistSort,
2a491182 293 getChannelSyncSort,
57c36b27
C
294 createSimilarityAttribute,
295 throwIfNotValid,
418d092a
C
296 buildServerIdsFollowedBy,
297 buildTrigramSearchIndex,
9f79ade6 298 buildWhereIdOrUUID,
3acc5084 299 isOutdated,
3caf77d3 300 parseAggregateResult,
bae61627 301 getInstanceFollowsSort,
5f3e2425 302 buildDirectionAndField,
e0a92917 303 createSafeIn,
bae61627
C
304 searchAttribute,
305 parseRowCountResult
57c36b27
C
306}
307
308// ---------------------------------------------------------------------------
309
310function searchTrigramNormalizeValue (value: string) {
2cebd797 311 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
57c36b27
C
312}
313
314function searchTrigramNormalizeCol (col: string) {
315 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
65fcc311 316}