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