]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/utils.ts
Update client dependencies
[github/Chocobozzz/PeerTube.git] / server / models / utils.ts
CommitLineData
16c016e8 1import { literal, Op, OrderItem, Sequelize } from 'sequelize'
c9f27d98 2import { Col } from 'sequelize/types/utils'
49cff3a4 3import validator from 'validator'
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
11 let finalField: string | Col
4b54f136
C
12
13 if (field.toLowerCase() === 'match') { // Search
1735c825 14 finalField = Sequelize.col('similarity')
aa3796bd
C
15 } else if (field === 'videoQuotaUsed') { // Users list
16 finalField = Sequelize.col('videoQuotaUsed')
1735c825
C
17 } else {
18 finalField = field
4b54f136 19 }
5c39adb7 20
1735c825 21 return [ [ finalField, direction ], lastSort ]
9a629c6e
C
22}
23
49cff3a4
C
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
c1125bca
RK
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
1735c825
C
47function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
48 const { direction, field } = buildDirectionAndField(value)
5c39adb7 49
1735c825 50 if (field.toLowerCase() === 'trending') { // Sort by aggregation
9a629c6e
C
51 return [
52 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
53
54 [ Sequelize.col('VideoModel.views'), direction ],
55
8e286cdc
RK
56 lastSort
57 ]
58 } else if (field === 'publishedAt') {
59 return [
60 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
61
62 [ Sequelize.col('VideoModel.publishedAt'), direction ],
63
9a629c6e
C
64 lastSort
65 ]
66 }
67
1735c825
C
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
0c691a18
C
77 const firstSort: OrderItem = typeof finalField === 'string'
78 ? finalField.split('.').concat([ direction ]) as OrderItem
1735c825 79 : [ finalField, direction ]
afa4374a
C
80
81 return [ firstSort, lastSort ]
5c39adb7
C
82}
83
95153292 84function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
1735c825 85 const [ firstSort ] = getSort(value)
792dbaf0 86
0c691a18 87 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[]
3bb6c526 88 return [ firstSort, lastSort ]
792dbaf0
GS
89}
90
cb5ce4cb
C
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
9f79ade6 104function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
304a84d5
C
105 if (!model.createdAt || !model.updatedAt) {
106 throw new Error('Miss createdAt & updatedAt attribuets to model')
107 }
108
9f79ade6
C
109 const now = Date.now()
110 const createdAtTime = model.createdAt.getTime()
111 const updatedAtTime = model.updatedAt.getTime()
112
113 return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
114}
115
1735c825
C
116function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
117 if (nullable && (value === null || value === undefined)) return
118
3fd3ab2d
C
119 if (validator(value) === false) {
120 throw new Error(`"${value}" is not a valid ${fieldName}.`)
121 }
122}
123
57c36b27
C
124function buildTrigramSearchIndex (indexName: string, attribute: string) {
125 return {
126 name: indexName,
b49f22d8
C
127 // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function
128 fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ],
57c36b27
C
129 using: 'gin',
130 operator: 'gin_trgm_ops'
131 }
132}
133
134function createSimilarityAttribute (col: string, value: string) {
135 return Sequelize.fn(
136 'similarity',
137
138 searchTrigramNormalizeCol(col),
139
140 searchTrigramNormalizeValue(value)
141 )
142}
143
696d83fd 144function buildBlockedAccountSQL (blockerIds: number[]) {
7ad9b984
C
145 const blockerIdsString = blockerIds.join(', ')
146
418d092a 147 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
9d6b9d10 148 ' UNION ' +
7ad9b984
C
149 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
150 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
151 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
418d092a
C
152}
153
1c58423f
C
154function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) {
155 const blockerIdsString = blockerIds.join(', ')
156
157 return [
158 literal(
159 `NOT EXISTS (` +
160 ` SELECT 1 FROM "accountBlocklist" ` +
161 ` WHERE "targetAccountId" = ${columnNameJoin} ` +
162 ` AND "accountId" IN (${blockerIdsString})` +
163 `)`
164 ),
165
166 literal(
167 `NOT EXISTS (` +
168 ` SELECT 1 FROM "account" ` +
169 ` INNER JOIN "actor" ON account."actorId" = actor.id ` +
170 ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` +
171 ` WHERE "account"."id" = ${columnNameJoin} ` +
172 ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` +
173 `)`
174 )
175 ]
176}
177
418d092a
C
178function buildServerIdsFollowedBy (actorId: any) {
179 const actorIdNumber = parseInt(actorId + '', 10)
180
181 return '(' +
182 'SELECT "actor"."serverId" FROM "actorFollow" ' +
183 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
184 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
a1587156 185 ')'
418d092a 186}
7ad9b984 187
418d092a
C
188function buildWhereIdOrUUID (id: number | string) {
189 return validator.isInt('' + id) ? { id } : { uuid: id }
7ad9b984
C
190}
191
3acc5084
C
192function parseAggregateResult (result: any) {
193 if (!result) return 0
194
195 const total = parseInt(result + '', 10)
196 if (isNaN(total)) return 0
197
198 return total
199}
200
16c016e8 201function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
5f3e2425
C
202 return stringArr.map(t => {
203 return t === null
204 ? null
16c016e8 205 : sequelize.escape('' + t)
5f3e2425 206 }).join(', ')
3caf77d3
C
207}
208
6b9c966f
C
209function buildLocalAccountIdsIn () {
210 return literal(
211 '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
212 )
213}
214
215function buildLocalActorIdsIn () {
216 return literal(
217 '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
218 )
219}
220
5f3e2425
C
221function buildDirectionAndField (value: string) {
222 let field: string
223 let direction: 'ASC' | 'DESC'
224
225 if (value.substring(0, 1) === '-') {
226 direction = 'DESC'
227 field = value.substring(1)
228 } else {
229 direction = 'ASC'
230 field = value
231 }
232
233 return { direction, field }
234}
235
feb34f6b
C
236function searchAttribute (sourceField?: string, targetField?: string) {
237 if (!sourceField) return {}
0d3a2982
RK
238
239 return {
feb34f6b 240 [targetField]: {
c0d2eac3
C
241 // FIXME: ts error
242 [Op.iLike as any]: `%${sourceField}%`
feb34f6b 243 }
0d3a2982
RK
244 }
245}
246
5c39adb7
C
247// ---------------------------------------------------------------------------
248
65fcc311 249export {
7ad9b984 250 buildBlockedAccountSQL,
1c58423f 251 buildBlockedAccountSQLOptimized,
6b9c966f 252 buildLocalActorIdsIn,
49cff3a4 253 getPlaylistSort,
06215f15 254 SortType,
6b9c966f 255 buildLocalAccountIdsIn,
792dbaf0 256 getSort,
c1125bca 257 getCommentSort,
9a629c6e 258 getVideoSort,
95153292 259 getBlacklistSort,
57c36b27
C
260 createSimilarityAttribute,
261 throwIfNotValid,
418d092a
C
262 buildServerIdsFollowedBy,
263 buildTrigramSearchIndex,
9f79ade6 264 buildWhereIdOrUUID,
3acc5084 265 isOutdated,
3caf77d3 266 parseAggregateResult,
cb5ce4cb 267 getFollowsSort,
5f3e2425 268 buildDirectionAndField,
e0a92917 269 createSafeIn,
feb34f6b 270 searchAttribute
57c36b27
C
271}
272
273// ---------------------------------------------------------------------------
274
275function searchTrigramNormalizeValue (value: string) {
2cebd797 276 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
57c36b27
C
277}
278
279function searchTrigramNormalizeCol (col: string) {
280 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
65fcc311 281}