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