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