]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - server/models/utils.ts
Typo
[github/Chocobozzz/PeerTube.git] / server / models / utils.ts
... / ...
CommitLineData
1import { literal, Op, OrderItem, Sequelize } from 'sequelize'
2import validator from 'validator'
3
4type SortType = { sortModel: string, sortValue: string }
5
6// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
7function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
8 const { direction, field } = buildDirectionAndField(value)
9
10 let finalField: string | ReturnType<typeof Sequelize.col>
11
12 if (field.toLowerCase() === 'match') { // Search
13 finalField = Sequelize.col('similarity')
14 } else if (field === 'videoQuotaUsed') { // Users list
15 finalField = Sequelize.col('videoQuotaUsed')
16 } else {
17 finalField = field
18 }
19
20 return [ [ finalField, direction ], lastSort ]
21}
22
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
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
46function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
47 const { direction, field } = buildDirectionAndField(value)
48
49 if (field.toLowerCase() === 'trending') { // Sort by aggregation
50 return [
51 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
52
53 [ Sequelize.col('VideoModel.views'), direction ],
54
55 lastSort
56 ]
57 } else if (field === 'publishedAt') {
58 return [
59 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
60
61 [ Sequelize.col('VideoModel.publishedAt'), direction ],
62
63 lastSort
64 ]
65 }
66
67 let finalField: string | ReturnType<typeof Sequelize.col>
68
69 // Alias
70 if (field.toLowerCase() === 'match') { // Search
71 finalField = Sequelize.col('similarity')
72 } else {
73 finalField = field
74 }
75
76 const firstSort: OrderItem = typeof finalField === 'string'
77 ? finalField.split('.').concat([ direction ]) as OrderItem
78 : [ finalField, direction ]
79
80 return [ firstSort, lastSort ]
81}
82
83function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
84 const [ firstSort ] = getSort(value)
85
86 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[]
87 return [ firstSort, lastSort ]
88}
89
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
103function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
104 if (!model.createdAt || !model.updatedAt) {
105 throw new Error('Miss createdAt & updatedAt attribuets to model')
106 }
107
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
115function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
116 if (nullable && (value === null || value === undefined)) return
117
118 if (validator(value) === false) {
119 throw new Error(`"${value}" is not a valid ${fieldName}.`)
120 }
121}
122
123function buildTrigramSearchIndex (indexName: string, attribute: string) {
124 return {
125 name: indexName,
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 ],
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
143function buildBlockedAccountSQL (blockerIds: number[]) {
144 const blockerIdsString = blockerIds.join(', ')
145
146 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
147 ' UNION ' +
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 + ')'
151}
152
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
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 +
184 ')'
185}
186
187function buildWhereIdOrUUID (id: number | string) {
188 return validator.isInt('' + id) ? { id } : { uuid: id }
189}
190
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
200function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
201 return stringArr.map(t => {
202 return t === null
203 ? null
204 : sequelize.escape('' + t)
205 }).join(', ')
206}
207
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
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
235function searchAttribute (sourceField?: string, targetField?: string) {
236 if (!sourceField) return {}
237
238 return {
239 [targetField]: {
240 // FIXME: ts error
241 [Op.iLike as any]: `%${sourceField}%`
242 }
243 }
244}
245
246// ---------------------------------------------------------------------------
247
248export {
249 buildBlockedAccountSQL,
250 buildBlockedAccountSQLOptimized,
251 buildLocalActorIdsIn,
252 getPlaylistSort,
253 SortType,
254 buildLocalAccountIdsIn,
255 getSort,
256 getCommentSort,
257 getVideoSort,
258 getBlacklistSort,
259 createSimilarityAttribute,
260 throwIfNotValid,
261 buildServerIdsFollowedBy,
262 buildTrigramSearchIndex,
263 buildWhereIdOrUUID,
264 isOutdated,
265 parseAggregateResult,
266 getFollowsSort,
267 buildDirectionAndField,
268 createSafeIn,
269 searchAttribute
270}
271
272// ---------------------------------------------------------------------------
273
274function searchTrigramNormalizeValue (value: string) {
275 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
276}
277
278function searchTrigramNormalizeCol (col: string) {
279 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
280}