]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/models/utils.ts
Merge branch 'release/3.2.0' into develop
[github/Chocobozzz/PeerTube.git] / server / models / utils.ts
... / ...
CommitLineData
1import { literal, Op, OrderItem, Sequelize } from 'sequelize'
2import { Col } from 'sequelize/types/lib/utils'
3import validator from 'validator'
4
5type SortType = { sortModel: string, sortValue: string }
6
7// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
8function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
9 const { direction, field } = buildDirectionAndField(value)
10
11 let finalField: string | Col
12
13 if (field.toLowerCase() === 'match') { // Search
14 finalField = Sequelize.col('similarity')
15 } else if (field === 'videoQuotaUsed') { // Users list
16 finalField = Sequelize.col('videoQuotaUsed')
17 } else {
18 finalField = field
19 }
20
21 return [ [ finalField, direction ], lastSort ]
22}
23
24function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
25 const { direction, field } = buildDirectionAndField(value)
26
27 if (field.toLowerCase() === 'name') {
28 return [ [ 'displayName', direction ], lastSort ]
29 }
30
31 return getSort(value, lastSort)
32}
33
34function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
35 const { direction, field } = buildDirectionAndField(value)
36
37 if (field === 'totalReplies') {
38 return [
39 [ Sequelize.literal('"totalReplies"'), direction ],
40 lastSort
41 ]
42 }
43
44 return getSort(value, lastSort)
45}
46
47function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
48 const { direction, field } = buildDirectionAndField(value)
49
50 if (field.toLowerCase() === 'trending') { // Sort by aggregation
51 return [
52 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
53
54 [ Sequelize.col('VideoModel.views'), direction ],
55
56 lastSort
57 ]
58 } else if (field === 'publishedAt') {
59 return [
60 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
61
62 [ Sequelize.col('VideoModel.publishedAt'), direction ],
63
64 lastSort
65 ]
66 }
67
68 let finalField: string | Col
69
70 // Alias
71 if (field.toLowerCase() === 'match') { // Search
72 finalField = Sequelize.col('similarity')
73 } else {
74 finalField = field
75 }
76
77 const firstSort = typeof finalField === 'string'
78 ? finalField.split('.').concat([ direction ]) as any // FIXME: sequelize typings
79 : [ finalField, direction ]
80
81 return [ firstSort, lastSort ]
82}
83
84function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
85 const [ firstSort ] = getSort(value)
86
87 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as any[] // FIXME: typings
88 return [ firstSort, lastSort ]
89}
90
91function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
92 const { direction, field } = buildDirectionAndField(value)
93
94 if (field === 'redundancyAllowed') {
95 return [
96 [ 'ActorFollowing', 'Server', 'redundancyAllowed', direction ],
97 lastSort
98 ]
99 }
100
101 return getSort(value, lastSort)
102}
103
104function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
105 const now = Date.now()
106 const createdAtTime = model.createdAt.getTime()
107 const updatedAtTime = model.updatedAt.getTime()
108
109 return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
110}
111
112function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
113 if (nullable && (value === null || value === undefined)) return
114
115 if (validator(value) === false) {
116 throw new Error(`"${value}" is not a valid ${fieldName}.`)
117 }
118}
119
120function buildTrigramSearchIndex (indexName: string, attribute: string) {
121 return {
122 name: indexName,
123 // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function
124 fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ],
125 using: 'gin',
126 operator: 'gin_trgm_ops'
127 }
128}
129
130function createSimilarityAttribute (col: string, value: string) {
131 return Sequelize.fn(
132 'similarity',
133
134 searchTrigramNormalizeCol(col),
135
136 searchTrigramNormalizeValue(value)
137 )
138}
139
140function buildBlockedAccountSQL (blockerIds: number[]) {
141 const blockerIdsString = blockerIds.join(', ')
142
143 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
144 ' UNION ' +
145 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
146 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
147 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
148}
149
150function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) {
151 const blockerIdsString = blockerIds.join(', ')
152
153 return [
154 literal(
155 `NOT EXISTS (` +
156 ` SELECT 1 FROM "accountBlocklist" ` +
157 ` WHERE "targetAccountId" = ${columnNameJoin} ` +
158 ` AND "accountId" IN (${blockerIdsString})` +
159 `)`
160 ),
161
162 literal(
163 `NOT EXISTS (` +
164 ` SELECT 1 FROM "account" ` +
165 ` INNER JOIN "actor" ON account."actorId" = actor.id ` +
166 ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` +
167 ` WHERE "account"."id" = ${columnNameJoin} ` +
168 ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` +
169 `)`
170 )
171 ]
172}
173
174function buildServerIdsFollowedBy (actorId: any) {
175 const actorIdNumber = parseInt(actorId + '', 10)
176
177 return '(' +
178 'SELECT "actor"."serverId" FROM "actorFollow" ' +
179 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
180 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
181 ')'
182}
183
184function buildWhereIdOrUUID (id: number | string) {
185 return validator.isInt('' + id) ? { id } : { uuid: id }
186}
187
188function parseAggregateResult (result: any) {
189 if (!result) return 0
190
191 const total = parseInt(result + '', 10)
192 if (isNaN(total)) return 0
193
194 return total
195}
196
197function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
198 return stringArr.map(t => {
199 return t === null
200 ? null
201 : sequelize.escape('' + t)
202 }).join(', ')
203}
204
205function buildLocalAccountIdsIn () {
206 return literal(
207 '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
208 )
209}
210
211function buildLocalActorIdsIn () {
212 return literal(
213 '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
214 )
215}
216
217function buildDirectionAndField (value: string) {
218 let field: string
219 let direction: 'ASC' | 'DESC'
220
221 if (value.substring(0, 1) === '-') {
222 direction = 'DESC'
223 field = value.substring(1)
224 } else {
225 direction = 'ASC'
226 field = value
227 }
228
229 return { direction, field }
230}
231
232function searchAttribute (sourceField?: string, targetField?: string) {
233 if (!sourceField) return {}
234
235 return {
236 [targetField]: {
237 [Op.iLike]: `%${sourceField}%`
238 }
239 }
240}
241
242// ---------------------------------------------------------------------------
243
244export {
245 buildBlockedAccountSQL,
246 buildBlockedAccountSQLOptimized,
247 buildLocalActorIdsIn,
248 getPlaylistSort,
249 SortType,
250 buildLocalAccountIdsIn,
251 getSort,
252 getCommentSort,
253 getVideoSort,
254 getBlacklistSort,
255 createSimilarityAttribute,
256 throwIfNotValid,
257 buildServerIdsFollowedBy,
258 buildTrigramSearchIndex,
259 buildWhereIdOrUUID,
260 isOutdated,
261 parseAggregateResult,
262 getFollowsSort,
263 buildDirectionAndField,
264 createSafeIn,
265 searchAttribute
266}
267
268// ---------------------------------------------------------------------------
269
270function searchTrigramNormalizeValue (value: string) {
271 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
272}
273
274function searchTrigramNormalizeCol (col: string) {
275 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
276}