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