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