]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/utils.ts
Fix comments SQL pagination
[github/Chocobozzz/PeerTube.git] / server / models / utils.ts
CommitLineData
eb66ee88 1import { literal, Model, ModelStatic, Op, OrderItem, Sequelize } from 'sequelize'
49cff3a4 2import validator from 'validator'
4638cd71 3import { forceNumber } from '@shared/core-utils'
eb66ee88 4import { AttributesOnly } from '@shared/typescript-utils'
57c36b27 5
95153292 6type SortType = { sortModel: string, sortValue: string }
06215f15 7
9a629c6e 8// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
1735c825
C
9function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
10 const { direction, field } = buildDirectionAndField(value)
11
9d8ef212 12 let finalField: string | ReturnType<typeof Sequelize.col>
4b54f136
C
13
14 if (field.toLowerCase() === 'match') { // Search
1735c825
C
15 finalField = Sequelize.col('similarity')
16 } else {
17 finalField = field
4b54f136 18 }
5c39adb7 19
1735c825 20 return [ [ finalField, direction ], lastSort ]
9a629c6e
C
21}
22
87a0cac6
C
23function getAdminUsersSort (value: string): OrderItem[] {
24 const { direction, field } = buildDirectionAndField(value)
25
26 let finalField: string | ReturnType<typeof Sequelize.col>
27
28 if (field === 'videoQuotaUsed') { // Users list
29 finalField = Sequelize.col('videoQuotaUsed')
30 } else {
31 finalField = field
32 }
33
34 const nullPolicy = direction === 'ASC'
35 ? 'NULLS FIRST'
36 : 'NULLS LAST'
37
38 // FIXME: typings
39 return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
40}
41
49cff3a4
C
42function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
43 const { direction, field } = buildDirectionAndField(value)
44
45 if (field.toLowerCase() === 'name') {
46 return [ [ 'displayName', direction ], lastSort ]
47 }
48
49 return getSort(value, lastSort)
50}
51
c1125bca
RK
52function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
53 const { direction, field } = buildDirectionAndField(value)
54
55 if (field === 'totalReplies') {
56 return [
57 [ Sequelize.literal('"totalReplies"'), direction ],
58 lastSort
59 ]
60 }
61
62 return getSort(value, lastSort)
63}
64
1735c825
C
65function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
66 const { direction, field } = buildDirectionAndField(value)
5c39adb7 67
1735c825 68 if (field.toLowerCase() === 'trending') { // Sort by aggregation
9a629c6e
C
69 return [
70 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
71
72 [ Sequelize.col('VideoModel.views'), direction ],
73
8e286cdc
RK
74 lastSort
75 ]
76 } else if (field === 'publishedAt') {
77 return [
78 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
79
80 [ Sequelize.col('VideoModel.publishedAt'), direction ],
81
9a629c6e
C
82 lastSort
83 ]
84 }
85
9d8ef212 86 let finalField: string | ReturnType<typeof Sequelize.col>
1735c825
C
87
88 // Alias
89 if (field.toLowerCase() === 'match') { // Search
90 finalField = Sequelize.col('similarity')
91 } else {
92 finalField = field
93 }
94
0c691a18
C
95 const firstSort: OrderItem = typeof finalField === 'string'
96 ? finalField.split('.').concat([ direction ]) as OrderItem
1735c825 97 : [ finalField, direction ]
afa4374a
C
98
99 return [ firstSort, lastSort ]
5c39adb7
C
100}
101
95153292 102function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
1735c825 103 const [ firstSort ] = getSort(value)
792dbaf0 104
0c691a18 105 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[]
3bb6c526 106 return [ firstSort, lastSort ]
792dbaf0
GS
107}
108
bae61627 109function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
cb5ce4cb
C
110 const { direction, field } = buildDirectionAndField(value)
111
112 if (field === 'redundancyAllowed') {
113 return [
bae61627 114 [ 'ActorFollowing.Server.redundancyAllowed', direction ],
cb5ce4cb
C
115 lastSort
116 ]
117 }
118
119 return getSort(value, lastSort)
120}
121
2a491182
F
122function getChannelSyncSort (value: string): OrderItem[] {
123 const { direction, field } = buildDirectionAndField(value)
124 if (field.toLowerCase() === 'videochannel') {
125 return [
126 [ literal('"VideoChannel.name"'), direction ]
127 ]
128 }
129 return [ [ field, direction ] ]
130}
131
9f79ade6 132function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
304a84d5 133 if (!model.createdAt || !model.updatedAt) {
7a4fd56c 134 throw new Error('Miss createdAt & updatedAt attributes to model')
304a84d5
C
135 }
136
9f79ade6
C
137 const now = Date.now()
138 const createdAtTime = model.createdAt.getTime()
139 const updatedAtTime = model.updatedAt.getTime()
140
141 return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
142}
143
1735c825
C
144function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
145 if (nullable && (value === null || value === undefined)) return
146
3fd3ab2d
C
147 if (validator(value) === false) {
148 throw new Error(`"${value}" is not a valid ${fieldName}.`)
149 }
150}
151
57c36b27
C
152function buildTrigramSearchIndex (indexName: string, attribute: string) {
153 return {
154 name: indexName,
b49f22d8
C
155 // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function
156 fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ],
57c36b27
C
157 using: 'gin',
158 operator: 'gin_trgm_ops'
159 }
160}
161
162function createSimilarityAttribute (col: string, value: string) {
163 return Sequelize.fn(
164 'similarity',
165
166 searchTrigramNormalizeCol(col),
167
168 searchTrigramNormalizeValue(value)
169 )
170}
171
696d83fd 172function buildBlockedAccountSQL (blockerIds: number[]) {
7ad9b984
C
173 const blockerIdsString = blockerIds.join(', ')
174
418d092a 175 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
9d6b9d10 176 ' UNION ' +
7ad9b984
C
177 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
178 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
179 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
418d092a
C
180}
181
182function buildServerIdsFollowedBy (actorId: any) {
4638cd71 183 const actorIdNumber = forceNumber(actorId)
418d092a
C
184
185 return '(' +
186 'SELECT "actor"."serverId" FROM "actorFollow" ' +
187 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
188 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
d0800f76 189 ')'
418d092a 190}
7ad9b984 191
418d092a
C
192function buildWhereIdOrUUID (id: number | string) {
193 return validator.isInt('' + id) ? { id } : { uuid: id }
7ad9b984
C
194}
195
3acc5084
C
196function parseAggregateResult (result: any) {
197 if (!result) return 0
198
4638cd71 199 const total = forceNumber(result)
3acc5084
C
200 if (isNaN(total)) return 0
201
202 return total
203}
204
bae61627
C
205function parseRowCountResult (result: any) {
206 if (result.length !== 0) return result[0].total
207
208 return 0
209}
210
cde3d90d
C
211function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) {
212 return toEscape.map(t => {
5f3e2425
C
213 return t === null
214 ? null
16c016e8 215 : sequelize.escape('' + t)
cde3d90d 216 }).concat(additionalUnescaped).join(', ')
3caf77d3
C
217}
218
6b9c966f
C
219function buildLocalAccountIdsIn () {
220 return literal(
221 '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
222 )
223}
224
225function buildLocalActorIdsIn () {
226 return literal(
227 '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
228 )
229}
230
5f3e2425
C
231function buildDirectionAndField (value: string) {
232 let field: string
233 let direction: 'ASC' | 'DESC'
234
235 if (value.substring(0, 1) === '-') {
236 direction = 'DESC'
237 field = value.substring(1)
238 } else {
239 direction = 'ASC'
240 field = value
241 }
242
243 return { direction, field }
244}
245
feb34f6b
C
246function searchAttribute (sourceField?: string, targetField?: string) {
247 if (!sourceField) return {}
0d3a2982
RK
248
249 return {
feb34f6b 250 [targetField]: {
c0d2eac3
C
251 // FIXME: ts error
252 [Op.iLike as any]: `%${sourceField}%`
feb34f6b 253 }
0d3a2982
RK
254 }
255}
256
eb66ee88
C
257function buildSQLAttributes <M extends Model> (options: {
258 model: ModelStatic<M>
259 tableName: string
260
f99a3f7d 261 excludeAttributes?: Exclude<keyof AttributesOnly<M>, symbol>[]
eb66ee88
C
262 aliasPrefix?: string
263}) {
264 const { model, tableName, aliasPrefix, excludeAttributes } = options
265
f99a3f7d 266 const attributes = Object.keys(model.getAttributes()) as Exclude<keyof AttributesOnly<M>, symbol>[]
eb66ee88
C
267
268 return attributes
269 .filter(a => {
270 if (!excludeAttributes) return true
271 if (excludeAttributes.includes(a)) return false
272
273 return true
274 })
275 .map(a => {
276 return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"`
277 })
278}
279
5c39adb7
C
280// ---------------------------------------------------------------------------
281
65fcc311 282export {
eb66ee88 283 buildSQLAttributes,
7ad9b984 284 buildBlockedAccountSQL,
6b9c966f 285 buildLocalActorIdsIn,
49cff3a4 286 getPlaylistSort,
06215f15 287 SortType,
6b9c966f 288 buildLocalAccountIdsIn,
792dbaf0 289 getSort,
c1125bca 290 getCommentSort,
87a0cac6 291 getAdminUsersSort,
9a629c6e 292 getVideoSort,
95153292 293 getBlacklistSort,
2a491182 294 getChannelSyncSort,
57c36b27
C
295 createSimilarityAttribute,
296 throwIfNotValid,
418d092a
C
297 buildServerIdsFollowedBy,
298 buildTrigramSearchIndex,
9f79ade6 299 buildWhereIdOrUUID,
3acc5084 300 isOutdated,
3caf77d3 301 parseAggregateResult,
bae61627 302 getInstanceFollowsSort,
5f3e2425 303 buildDirectionAndField,
e0a92917 304 createSafeIn,
bae61627
C
305 searchAttribute,
306 parseRowCountResult
57c36b27
C
307}
308
309// ---------------------------------------------------------------------------
310
311function searchTrigramNormalizeValue (value: string) {
2cebd797 312 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
57c36b27
C
313}
314
315function searchTrigramNormalizeCol (col: string) {
316 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
65fcc311 317}