]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/utils.ts
Fix broken dep
[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
cb5ce4cb
C
90function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
91 const { direction, field } = buildDirectionAndField(value)
92
93 if (field === 'redundancyAllowed') {
94 return [
95 [ 'ActorFollowing', 'Server', 'redundancyAllowed', direction ],
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 +
a1587156 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
16c016e8 200function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
5f3e2425
C
201 return stringArr.map(t => {
202 return t === null
203 ? null
16c016e8 204 : sequelize.escape('' + t)
5f3e2425 205 }).join(', ')
3caf77d3
C
206}
207
6b9c966f
C
208function buildLocalAccountIdsIn () {
209 return literal(
210 '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
211 )
212}
213
214function buildLocalActorIdsIn () {
215 return literal(
216 '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
217 )
218}
219
5f3e2425
C
220function buildDirectionAndField (value: string) {
221 let field: string
222 let direction: 'ASC' | 'DESC'
223
224 if (value.substring(0, 1) === '-') {
225 direction = 'DESC'
226 field = value.substring(1)
227 } else {
228 direction = 'ASC'
229 field = value
230 }
231
232 return { direction, field }
233}
234
feb34f6b
C
235function searchAttribute (sourceField?: string, targetField?: string) {
236 if (!sourceField) return {}
0d3a2982
RK
237
238 return {
feb34f6b 239 [targetField]: {
c0d2eac3
C
240 // FIXME: ts error
241 [Op.iLike as any]: `%${sourceField}%`
feb34f6b 242 }
0d3a2982
RK
243 }
244}
245
5c39adb7
C
246// ---------------------------------------------------------------------------
247
65fcc311 248export {
7ad9b984 249 buildBlockedAccountSQL,
1c58423f 250 buildBlockedAccountSQLOptimized,
6b9c966f 251 buildLocalActorIdsIn,
49cff3a4 252 getPlaylistSort,
06215f15 253 SortType,
6b9c966f 254 buildLocalAccountIdsIn,
792dbaf0 255 getSort,
c1125bca 256 getCommentSort,
9a629c6e 257 getVideoSort,
95153292 258 getBlacklistSort,
57c36b27
C
259 createSimilarityAttribute,
260 throwIfNotValid,
418d092a
C
261 buildServerIdsFollowedBy,
262 buildTrigramSearchIndex,
9f79ade6 263 buildWhereIdOrUUID,
3acc5084 264 isOutdated,
3caf77d3 265 parseAggregateResult,
cb5ce4cb 266 getFollowsSort,
5f3e2425 267 buildDirectionAndField,
e0a92917 268 createSafeIn,
feb34f6b 269 searchAttribute
57c36b27
C
270}
271
272// ---------------------------------------------------------------------------
273
274function searchTrigramNormalizeValue (value: string) {
2cebd797 275 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
57c36b27
C
276}
277
278function searchTrigramNormalizeCol (col: string) {
279 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
65fcc311 280}