From 8c4bbd946d2247c2e239cbbf8773d2d31c1a57aa Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 10 Jan 2023 11:09:30 +0100 Subject: [PATCH] Refactor model utils --- server/middlewares/sort.ts | 23 +- server/models/abuse/abuse-message.ts | 2 +- server/models/abuse/abuse.ts | 2 +- .../models/abuse/sql/abuse-query-builder.ts | 4 +- server/models/account/account-blocklist.ts | 2 +- server/models/account/account-video-rate.ts | 2 +- server/models/account/account.ts | 4 +- server/models/actor/actor-follow.ts | 4 +- server/models/actor/actor-image.ts | 2 +- server/models/actor/actor.ts | 4 +- .../instance-list-followers-query-builder.ts | 2 +- .../instance-list-following-query-builder.ts | 2 +- .../instance-list-follows-query-builder.ts | 2 +- server/models/redundancy/video-redundancy.ts | 2 +- server/models/server/plugin.ts | 2 +- server/models/server/server-blocklist.ts | 2 +- server/models/server/server.ts | 2 +- server/models/shared/index.ts | 4 + server/models/{ => shared}/model-cache.ts | 0 server/models/shared/query.ts | 75 ++++- server/models/shared/sequelize-helpers.ts | 39 +++ server/models/shared/sort.ts | 160 +++++++++ server/models/shared/sql.ts | 68 ++++ server/models/shared/update.ts | 14 +- .../user-notitication-list-query-builder.ts | 2 +- .../models/user/user-notification-setting.ts | 2 +- server/models/user/user-notification.ts | 2 +- server/models/user/user.ts | 4 +- server/models/utils.ts | 317 ------------------ .../video-comment-list-query-builder.ts | 2 +- .../shared/abstract-video-query-builder.ts | 2 +- .../sql/video/videos-id-list-query-builder.ts | 7 +- server/models/video/tag.ts | 2 +- server/models/video/video-blacklist.ts | 6 +- server/models/video/video-caption.ts | 2 +- server/models/video/video-change-ownership.ts | 2 +- server/models/video/video-channel-sync.ts | 2 +- server/models/video/video-channel.ts | 12 +- server/models/video/video-comment.ts | 2 +- server/models/video/video-file.ts | 11 +- server/models/video/video-import.ts | 2 +- server/models/video/video-playlist-element.ts | 2 +- server/models/video/video-playlist.ts | 12 +- server/models/video/video-share.ts | 2 +- .../models/video/video-streaming-playlist.ts | 7 +- server/models/video/video.ts | 7 +- 46 files changed, 418 insertions(+), 414 deletions(-) rename server/models/{ => shared}/model-cache.ts (100%) create mode 100644 server/models/shared/sequelize-helpers.ts create mode 100644 server/models/shared/sort.ts create mode 100644 server/models/shared/sql.ts delete mode 100644 server/models/utils.ts diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 458895898..77a532276 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts @@ -1,5 +1,4 @@ import express from 'express' -import { SortType } from '../models/utils' const setDefaultSort = setDefaultSortFactory('-createdAt') const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') @@ -7,27 +6,7 @@ const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') const setDefaultSearchSort = setDefaultSortFactory('-match') - -function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { - const newSort: SortType = { sortModel: undefined, sortValue: '' } - - if (!req.query.sort) req.query.sort = '-createdAt' - - // Set model we want to sort onto - if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' || - req.query.sort === '-id' || req.query.sort === 'id') { - // If we want to sort onto the BlacklistedVideos relation, we won't specify it in the query parameter... - newSort.sortModel = undefined - } else { - newSort.sortModel = 'Video' - } - - newSort.sortValue = req.query.sort - - req.query.sort = newSort - - return next() -} +const setBlacklistSort = setDefaultSortFactory('-createdAt') // --------------------------------------------------------------------------- diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 20008768b..14a5bffa2 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts @@ -5,7 +5,7 @@ import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' import { AbuseMessage } from '@shared/models' import { AttributesOnly } from '@shared/typescript-utils' import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' -import { getSort, throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../shared' import { AbuseModel } from './abuse' @Table({ diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 5610077c9..4ce40bf2f 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -34,7 +34,7 @@ import { AttributesOnly } from '@shared/typescript-utils' import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' -import { getSort, throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../shared' import { ThumbnailModel } from '../video/thumbnail' import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' import { VideoBlacklistModel } from '../video/video-blacklist' diff --git a/server/models/abuse/sql/abuse-query-builder.ts b/server/models/abuse/sql/abuse-query-builder.ts index 854f0cda8..282d4541a 100644 --- a/server/models/abuse/sql/abuse-query-builder.ts +++ b/server/models/abuse/sql/abuse-query-builder.ts @@ -2,7 +2,7 @@ import { exists } from '@server/helpers/custom-validators/misc' import { forceNumber } from '@shared/core-utils' import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' -import { buildBlockedAccountSQL, buildDirectionAndField } from '../../utils' +import { buildBlockedAccountSQL, buildSortDirectionAndField } from '../../shared' export type BuildAbusesQueryOptions = { start: number @@ -157,7 +157,7 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | } function buildAbuseOrder (value: string) { - const { direction, field } = buildDirectionAndField(value) + const { direction, field } = buildSortDirectionAndField(value) return `ORDER BY "abuse"."${field}" ${direction}` } diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 377249b38..f6212ff6e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts @@ -6,7 +6,7 @@ import { AttributesOnly } from '@shared/typescript-utils' import { AccountBlock } from '../../../shared/models' import { ActorModel } from '../actor/actor' import { ServerModel } from '../server/server' -import { createSafeIn, getSort, searchAttribute } from '../utils' +import { createSafeIn, getSort, searchAttribute } from '../shared' import { AccountModel } from './account' @Table({ diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 7afc907da..9e7ef4394 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -11,7 +11,7 @@ import { AttributesOnly } from '@shared/typescript-utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' import { ActorModel } from '../actor/actor' -import { getSort, throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' import { AccountModel } from './account' diff --git a/server/models/account/account.ts b/server/models/account/account.ts index fca1b7b6d..dc989417b 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -16,7 +16,7 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { ModelCache } from '@server/models/model-cache' +import { ModelCache } from '@server/models/shared/model-cache' import { AttributesOnly } from '@shared/typescript-utils' import { Account, AccountSummary } from '../../../shared/models/actors' import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' @@ -38,7 +38,7 @@ import { ApplicationModel } from '../application/application' import { ServerModel } from '../server/server' import { ServerBlocklistModel } from '../server/server-blocklist' import { UserModel } from '../user/user' -import { buildSQLAttributes, getSort, throwIfNotValid } from '../utils' +import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { VideoCommentModel } from '../video/video-comment' diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index a849817d5..32e5d78b0 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts @@ -38,7 +38,7 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM import { AccountModel } from '../account/account' import { ServerModel } from '../server/server' import { doesExist } from '../shared/query' -import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils' +import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' import { VideoChannelModel } from '../video/video-channel' import { ActorModel, unusedActorAttributesForAPI } from './actor' import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' @@ -225,7 +225,7 @@ export class ActorFollowModel extends Model { diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index 83898cd38..9c34a0101 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts @@ -22,7 +22,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp import { logger } from '../../helpers/logger' import { CONFIG } from '../../initializers/config' import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' -import { buildSQLAttributes, throwIfNotValid } from '../utils' +import { buildSQLAttributes, throwIfNotValid } from '../shared' import { ActorModel } from './actor' @Table({ diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index a62e6030a..1432e8757 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts @@ -17,7 +17,7 @@ import { } from 'sequelize-typescript' import { activityPubContextify } from '@server/lib/activitypub/context' import { getBiggestActorImage } from '@server/lib/actor-image' -import { ModelCache } from '@server/models/model-cache' +import { ModelCache } from '@server/models/shared/model-cache' import { forceNumber, getLowercaseExtension } from '@shared/core-utils' import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' import { AttributesOnly } from '@shared/typescript-utils' @@ -55,7 +55,7 @@ import { import { AccountModel } from '../account/account' import { getServerActor } from '../application/application' import { ServerModel } from '../server/server' -import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../utils' +import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { ActorFollowModel } from './actor-follow' diff --git a/server/models/actor/sql/instance-list-followers-query-builder.ts b/server/models/actor/sql/instance-list-followers-query-builder.ts index 4a17a8f11..34ce29b5d 100644 --- a/server/models/actor/sql/instance-list-followers-query-builder.ts +++ b/server/models/actor/sql/instance-list-followers-query-builder.ts @@ -1,8 +1,8 @@ import { Sequelize } from 'sequelize' import { ModelBuilder } from '@server/models/shared' -import { parseRowCountResult } from '@server/models/utils' import { MActorFollowActorsDefault } from '@server/types/models' import { ActivityPubActorType, FollowState } from '@shared/models' +import { parseRowCountResult } from '../../shared' import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' export interface ListFollowersOptions { diff --git a/server/models/actor/sql/instance-list-following-query-builder.ts b/server/models/actor/sql/instance-list-following-query-builder.ts index 880170b85..77b4e3dce 100644 --- a/server/models/actor/sql/instance-list-following-query-builder.ts +++ b/server/models/actor/sql/instance-list-following-query-builder.ts @@ -1,8 +1,8 @@ import { Sequelize } from 'sequelize' import { ModelBuilder } from '@server/models/shared' -import { parseRowCountResult } from '@server/models/utils' import { MActorFollowActorsDefault } from '@server/types/models' import { ActivityPubActorType, FollowState } from '@shared/models' +import { parseRowCountResult } from '../../shared' import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' export interface ListFollowingOptions { diff --git a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts index 1d70fbe70..d9593e48b 100644 --- a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts +++ b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts @@ -1,7 +1,7 @@ import { Sequelize } from 'sequelize' import { AbstractRunQuery } from '@server/models/shared' -import { getInstanceFollowsSort } from '@server/models/utils' import { ActorImageType } from '@shared/models' +import { getInstanceFollowsSort } from '../../../shared' import { ActorFollowTableAttributes } from './actor-follow-table-attributes' type BaseOptions = { diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 15909d5f3..c2a72b71f 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -34,7 +34,7 @@ import { CONFIG } from '../../initializers/config' import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' import { ActorModel } from '../actor/actor' import { ServerModel } from '../server/server' -import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' +import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared' import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 71c205ffa..9948c9f7a 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts @@ -11,7 +11,7 @@ import { isPluginStableVersionValid, isPluginTypeValid } from '../../helpers/custom-validators/plugins' -import { getSort, throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../shared' @DefaultScope(() => ({ attributes: { diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9752dfbc3..3d755fe4a 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts @@ -4,7 +4,7 @@ import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormat import { ServerBlock } from '@shared/models' import { AttributesOnly } from '@shared/typescript-utils' import { AccountModel } from '../account/account' -import { createSafeIn, getSort, searchAttribute } from '../utils' +import { createSafeIn, getSort, searchAttribute } from '../shared' import { ServerModel } from './server' enum ScopeNames { diff --git a/server/models/server/server.ts b/server/models/server/server.ts index f6cb40f60..a5e05f460 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts @@ -4,7 +4,7 @@ import { MServer, MServerFormattable } from '@server/types/models/server' import { AttributesOnly } from '@shared/typescript-utils' import { isHostValid } from '../../helpers/custom-validators/servers' import { ActorModel } from '../actor/actor' -import { buildSQLAttributes, throwIfNotValid } from '../utils' +import { buildSQLAttributes, throwIfNotValid } from '../shared' import { ServerBlocklistModel } from './server-blocklist' @Table({ diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 04528929c..5a7621e4d 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts @@ -1,4 +1,8 @@ export * from './abstract-run-query' export * from './model-builder' +export * from './model-cache' export * from './query' +export * from './sequelize-helpers' +export * from './sort' +export * from './sql' export * from './update' diff --git a/server/models/model-cache.ts b/server/models/shared/model-cache.ts similarity index 100% rename from server/models/model-cache.ts rename to server/models/shared/model-cache.ts diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts index 036cc13c6..934acc21f 100644 --- a/server/models/shared/query.ts +++ b/server/models/shared/query.ts @@ -1,17 +1,82 @@ -import { BindOrReplacements, QueryTypes } from 'sequelize' -import { sequelizeTypescript } from '@server/initializers/database' +import { BindOrReplacements, Op, QueryTypes, Sequelize } from 'sequelize' +import validator from 'validator' +import { forceNumber } from '@shared/core-utils' -function doesExist (query: string, bind?: BindOrReplacements) { +function doesExist (sequelize: Sequelize, query: string, bind?: BindOrReplacements) { const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind, raw: true } - return sequelizeTypescript.query(query, options) + return sequelize.query(query, options) .then(results => results.length === 1) } +function createSimilarityAttribute (col: string, value: string) { + return Sequelize.fn( + 'similarity', + + searchTrigramNormalizeCol(col), + + searchTrigramNormalizeValue(value) + ) +} + +function buildWhereIdOrUUID (id: number | string) { + return validator.isInt('' + id) ? { id } : { uuid: id } +} + +function parseAggregateResult (result: any) { + if (!result) return 0 + + const total = forceNumber(result) + if (isNaN(total)) return 0 + + return total +} + +function parseRowCountResult (result: any) { + if (result.length !== 0) return result[0].total + + return 0 +} + +function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) { + return toEscape.map(t => { + return t === null + ? null + : sequelize.escape('' + t) + }).concat(additionalUnescaped).join(', ') +} + +function searchAttribute (sourceField?: string, targetField?: string) { + if (!sourceField) return {} + + return { + [targetField]: { + // FIXME: ts error + [Op.iLike as any]: `%${sourceField}%` + } + } +} + export { - doesExist + doesExist, + createSimilarityAttribute, + buildWhereIdOrUUID, + parseAggregateResult, + parseRowCountResult, + createSafeIn, + searchAttribute +} + +// --------------------------------------------------------------------------- + +function searchTrigramNormalizeValue (value: string) { + return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) +} + +function searchTrigramNormalizeCol (col: string) { + return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) } diff --git a/server/models/shared/sequelize-helpers.ts b/server/models/shared/sequelize-helpers.ts new file mode 100644 index 000000000..7af8471dc --- /dev/null +++ b/server/models/shared/sequelize-helpers.ts @@ -0,0 +1,39 @@ +import { Sequelize } from 'sequelize' + +function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { + if (!model.createdAt || !model.updatedAt) { + throw new Error('Miss createdAt & updatedAt attributes to model') + } + + const now = Date.now() + const createdAtTime = model.createdAt.getTime() + const updatedAtTime = model.updatedAt.getTime() + + return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval +} + +function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { + if (nullable && (value === null || value === undefined)) return + + if (validator(value) === false) { + throw new Error(`"${value}" is not a valid ${fieldName}.`) + } +} + +function buildTrigramSearchIndex (indexName: string, attribute: string) { + return { + name: indexName, + // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function + fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], + using: 'gin', + operator: 'gin_trgm_ops' + } +} + +// --------------------------------------------------------------------------- + +export { + throwIfNotValid, + buildTrigramSearchIndex, + isOutdated +} diff --git a/server/models/shared/sort.ts b/server/models/shared/sort.ts new file mode 100644 index 000000000..77e84dcf4 --- /dev/null +++ b/server/models/shared/sort.ts @@ -0,0 +1,160 @@ +import { literal, OrderItem, Sequelize } from 'sequelize' + +// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] +function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + let finalField: string | ReturnType + + if (field.toLowerCase() === 'match') { // Search + finalField = Sequelize.col('similarity') + } else { + finalField = field + } + + return [ [ finalField, direction ], lastSort ] +} + +function getAdminUsersSort (value: string): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + let finalField: string | ReturnType + + if (field === 'videoQuotaUsed') { // Users list + finalField = Sequelize.col('videoQuotaUsed') + } else { + finalField = field + } + + const nullPolicy = direction === 'ASC' + ? 'NULLS FIRST' + : 'NULLS LAST' + + // FIXME: typings + return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] +} + +function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + if (field.toLowerCase() === 'name') { + return [ [ 'displayName', direction ], lastSort ] + } + + return getSort(value, lastSort) +} + +function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + if (field === 'totalReplies') { + return [ + [ Sequelize.literal('"totalReplies"'), direction ], + lastSort + ] + } + + return getSort(value, lastSort) +} + +function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + if (field.toLowerCase() === 'trending') { // Sort by aggregation + return [ + [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], + + [ Sequelize.col('VideoModel.views'), direction ], + + lastSort + ] + } else if (field === 'publishedAt') { + return [ + [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], + + [ Sequelize.col('VideoModel.publishedAt'), direction ], + + lastSort + ] + } + + let finalField: string | ReturnType + + // Alias + if (field.toLowerCase() === 'match') { // Search + finalField = Sequelize.col('similarity') + } else { + finalField = field + } + + const firstSort: OrderItem = typeof finalField === 'string' + ? finalField.split('.').concat([ direction ]) as OrderItem + : [ finalField, direction ] + + return [ firstSort, lastSort ] +} + +function getBlacklistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + const videoFields = new Set([ 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid' ]) + + if (videoFields.has(field)) { + return [ + [ literal(`"Video.${field}" ${direction}`) ], + lastSort + ] as OrderItem[] + } + + return getSort(value, lastSort) +} + +function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + + if (field === 'redundancyAllowed') { + return [ + [ 'ActorFollowing.Server.redundancyAllowed', direction ], + lastSort + ] + } + + return getSort(value, lastSort) +} + +function getChannelSyncSort (value: string): OrderItem[] { + const { direction, field } = buildSortDirectionAndField(value) + if (field.toLowerCase() === 'videochannel') { + return [ + [ literal('"VideoChannel.name"'), direction ] + ] + } + return [ [ field, direction ] ] +} + +function buildSortDirectionAndField (value: string) { + let field: string + let direction: 'ASC' | 'DESC' + + if (value.substring(0, 1) === '-') { + direction = 'DESC' + field = value.substring(1) + } else { + direction = 'ASC' + field = value + } + + return { direction, field } +} + +export { + buildSortDirectionAndField, + getPlaylistSort, + getSort, + getCommentSort, + getAdminUsersSort, + getVideoSort, + getBlacklistSort, + getChannelSyncSort, + getInstanceFollowsSort +} diff --git a/server/models/shared/sql.ts b/server/models/shared/sql.ts new file mode 100644 index 000000000..5aaeb49f0 --- /dev/null +++ b/server/models/shared/sql.ts @@ -0,0 +1,68 @@ +import { literal, Model, ModelStatic } from 'sequelize' +import { forceNumber } from '@shared/core-utils' +import { AttributesOnly } from '@shared/typescript-utils' + +function buildLocalAccountIdsIn () { + return literal( + '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' + ) +} + +function buildLocalActorIdsIn () { + return literal( + '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' + ) +} + +function buildBlockedAccountSQL (blockerIds: number[]) { + const blockerIdsString = blockerIds.join(', ') + + return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + + ' UNION ' + + 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + + 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + + 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' +} + +function buildServerIdsFollowedBy (actorId: any) { + const actorIdNumber = forceNumber(actorId) + + return '(' + + 'SELECT "actor"."serverId" FROM "actorFollow" ' + + 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + + 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + + ')' +} + +function buildSQLAttributes (options: { + model: ModelStatic + tableName: string + + excludeAttributes?: Exclude, symbol>[] + aliasPrefix?: string +}) { + const { model, tableName, aliasPrefix, excludeAttributes } = options + + const attributes = Object.keys(model.getAttributes()) as Exclude, symbol>[] + + return attributes + .filter(a => { + if (!excludeAttributes) return true + if (excludeAttributes.includes(a)) return false + + return true + }) + .map(a => { + return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"` + }) +} + +// --------------------------------------------------------------------------- + +export { + buildSQLAttributes, + buildBlockedAccountSQL, + buildServerIdsFollowedBy, + buildLocalAccountIdsIn, + buildLocalActorIdsIn +} diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts index d338211e3..d02c4535d 100644 --- a/server/models/shared/update.ts +++ b/server/models/shared/update.ts @@ -1,9 +1,15 @@ -import { QueryTypes, Transaction } from 'sequelize' -import { sequelizeTypescript } from '@server/initializers/database' +import { QueryTypes, Sequelize, Transaction } from 'sequelize' // Sequelize always skip the update if we only update updatedAt field -function setAsUpdated (table: string, id: number, transaction?: Transaction) { - return sequelizeTypescript.query( +function setAsUpdated (options: { + sequelize: Sequelize + table: string + id: number + transaction?: Transaction +}) { + const { sequelize, table, id, transaction } = options + + return sequelize.query( `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, { replacements: { table, id, updatedAt: new Date() }, diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts index 31b4932bf..d11546df0 100644 --- a/server/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/models/user/sql/user-notitication-list-query-builder.ts @@ -1,8 +1,8 @@ import { Sequelize } from 'sequelize' import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' -import { getSort } from '@server/models/utils' import { UserNotificationModelForApi } from '@server/types/models' import { ActorImageType } from '@shared/models' +import { getSort } from '../../shared' export interface ListNotificationsOptions { userId: number diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 66e1d85b3..394494c0c 100644 --- a/server/models/user/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts @@ -17,7 +17,7 @@ import { MNotificationSettingFormattable } from '@server/types/models' import { AttributesOnly } from '@shared/typescript-utils' import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' -import { throwIfNotValid } from '../utils' +import { throwIfNotValid } from '../shared' import { UserModel } from './user' @Table({ diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index d37fa5dc7..6e134158f 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts @@ -13,7 +13,7 @@ import { AccountModel } from '../account/account' import { ActorFollowModel } from '../actor/actor-follow' import { ApplicationModel } from '../application/application' import { PluginModel } from '../server/plugin' -import { throwIfNotValid } from '../utils' +import { throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { VideoBlacklistModel } from '../video/video-blacklist' import { VideoCommentModel } from '../video/video-comment' diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 672728a2a..0932a367a 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -30,6 +30,7 @@ import { MUserNotifSettingChannelDefault, MUserWithNotificationSetting } from '@server/types/models' +import { forceNumber } from '@shared/core-utils' import { AttributesOnly } from '@shared/typescript-utils' import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' @@ -63,14 +64,13 @@ import { ActorModel } from '../actor/actor' import { ActorFollowModel } from '../actor/actor-follow' import { ActorImageModel } from '../actor/actor-image' import { OAuthTokenModel } from '../oauth/oauth-token' -import { getAdminUsersSort, throwIfNotValid } from '../utils' +import { getAdminUsersSort, throwIfNotValid } from '../shared' import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { VideoImportModel } from '../video/video-import' import { VideoLiveModel } from '../video/video-live' import { VideoPlaylistModel } from '../video/video-playlist' import { UserNotificationSettingModel } from './user-notification-setting' -import { forceNumber } from '@shared/core-utils' enum ScopeNames { FOR_ME_API = 'FOR_ME_API', diff --git a/server/models/utils.ts b/server/models/utils.ts deleted file mode 100644 index 93723816f..000000000 --- a/server/models/utils.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { literal, Model, ModelStatic, Op, OrderItem, Sequelize } from 'sequelize' -import validator from 'validator' -import { forceNumber } from '@shared/core-utils' -import { AttributesOnly } from '@shared/typescript-utils' - -type SortType = { sortModel: string, sortValue: string } - -// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] -function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - let finalField: string | ReturnType - - if (field.toLowerCase() === 'match') { // Search - finalField = Sequelize.col('similarity') - } else { - finalField = field - } - - return [ [ finalField, direction ], lastSort ] -} - -function getAdminUsersSort (value: string): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - let finalField: string | ReturnType - - if (field === 'videoQuotaUsed') { // Users list - finalField = Sequelize.col('videoQuotaUsed') - } else { - finalField = field - } - - const nullPolicy = direction === 'ASC' - ? 'NULLS FIRST' - : 'NULLS LAST' - - // FIXME: typings - return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] -} - -function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - if (field.toLowerCase() === 'name') { - return [ [ 'displayName', direction ], lastSort ] - } - - return getSort(value, lastSort) -} - -function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - if (field === 'totalReplies') { - return [ - [ Sequelize.literal('"totalReplies"'), direction ], - lastSort - ] - } - - return getSort(value, lastSort) -} - -function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - if (field.toLowerCase() === 'trending') { // Sort by aggregation - return [ - [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], - - [ Sequelize.col('VideoModel.views'), direction ], - - lastSort - ] - } else if (field === 'publishedAt') { - return [ - [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], - - [ Sequelize.col('VideoModel.publishedAt'), direction ], - - lastSort - ] - } - - let finalField: string | ReturnType - - // Alias - if (field.toLowerCase() === 'match') { // Search - finalField = Sequelize.col('similarity') - } else { - finalField = field - } - - const firstSort: OrderItem = typeof finalField === 'string' - ? finalField.split('.').concat([ direction ]) as OrderItem - : [ finalField, direction ] - - return [ firstSort, lastSort ] -} - -function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const [ firstSort ] = getSort(value) - - if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[] - return [ firstSort, lastSort ] -} - -function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - - if (field === 'redundancyAllowed') { - return [ - [ 'ActorFollowing.Server.redundancyAllowed', direction ], - lastSort - ] - } - - return getSort(value, lastSort) -} - -function getChannelSyncSort (value: string): OrderItem[] { - const { direction, field } = buildDirectionAndField(value) - if (field.toLowerCase() === 'videochannel') { - return [ - [ literal('"VideoChannel.name"'), direction ] - ] - } - return [ [ field, direction ] ] -} - -function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { - if (!model.createdAt || !model.updatedAt) { - throw new Error('Miss createdAt & updatedAt attributes to model') - } - - const now = Date.now() - const createdAtTime = model.createdAt.getTime() - const updatedAtTime = model.updatedAt.getTime() - - return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval -} - -function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { - if (nullable && (value === null || value === undefined)) return - - if (validator(value) === false) { - throw new Error(`"${value}" is not a valid ${fieldName}.`) - } -} - -function buildTrigramSearchIndex (indexName: string, attribute: string) { - return { - name: indexName, - // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function - fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], - using: 'gin', - operator: 'gin_trgm_ops' - } -} - -function createSimilarityAttribute (col: string, value: string) { - return Sequelize.fn( - 'similarity', - - searchTrigramNormalizeCol(col), - - searchTrigramNormalizeValue(value) - ) -} - -function buildBlockedAccountSQL (blockerIds: number[]) { - const blockerIdsString = blockerIds.join(', ') - - return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + - ' UNION ' + - 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + - 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + - 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' -} - -function buildServerIdsFollowedBy (actorId: any) { - const actorIdNumber = forceNumber(actorId) - - return '(' + - 'SELECT "actor"."serverId" FROM "actorFollow" ' + - 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + - 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + - ')' -} - -function buildWhereIdOrUUID (id: number | string) { - return validator.isInt('' + id) ? { id } : { uuid: id } -} - -function parseAggregateResult (result: any) { - if (!result) return 0 - - const total = forceNumber(result) - if (isNaN(total)) return 0 - - return total -} - -function parseRowCountResult (result: any) { - if (result.length !== 0) return result[0].total - - return 0 -} - -function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) { - return toEscape.map(t => { - return t === null - ? null - : sequelize.escape('' + t) - }).concat(additionalUnescaped).join(', ') -} - -function buildLocalAccountIdsIn () { - return literal( - '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' - ) -} - -function buildLocalActorIdsIn () { - return literal( - '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' - ) -} - -function buildDirectionAndField (value: string) { - let field: string - let direction: 'ASC' | 'DESC' - - if (value.substring(0, 1) === '-') { - direction = 'DESC' - field = value.substring(1) - } else { - direction = 'ASC' - field = value - } - - return { direction, field } -} - -function searchAttribute (sourceField?: string, targetField?: string) { - if (!sourceField) return {} - - return { - [targetField]: { - // FIXME: ts error - [Op.iLike as any]: `%${sourceField}%` - } - } -} - -function buildSQLAttributes (options: { - model: ModelStatic - tableName: string - - excludeAttributes?: Exclude, symbol>[] - aliasPrefix?: string -}) { - const { model, tableName, aliasPrefix, excludeAttributes } = options - - const attributes = Object.keys(model.getAttributes()) as Exclude, symbol>[] - - return attributes - .filter(a => { - if (!excludeAttributes) return true - if (excludeAttributes.includes(a)) return false - - return true - }) - .map(a => { - return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"` - }) -} - -// --------------------------------------------------------------------------- - -export { - buildSQLAttributes, - buildBlockedAccountSQL, - buildLocalActorIdsIn, - getPlaylistSort, - SortType, - buildLocalAccountIdsIn, - getSort, - getCommentSort, - getAdminUsersSort, - getVideoSort, - getBlacklistSort, - getChannelSyncSort, - createSimilarityAttribute, - throwIfNotValid, - buildServerIdsFollowedBy, - buildTrigramSearchIndex, - buildWhereIdOrUUID, - isOutdated, - parseAggregateResult, - getInstanceFollowsSort, - buildDirectionAndField, - createSafeIn, - searchAttribute, - parseRowCountResult -} - -// --------------------------------------------------------------------------- - -function searchTrigramNormalizeValue (value: string) { - return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) -} - -function searchTrigramNormalizeCol (col: string) { - return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) -} diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts index 49b41b6d8..3960f6b13 100644 --- a/server/models/video/sql/comment/video-comment-list-query-builder.ts +++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts @@ -1,7 +1,7 @@ import { Model, Sequelize, Transaction } from 'sequelize' import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' -import { createSafeIn, getCommentSort, parseRowCountResult } from '@server/models/utils' import { ActorImageType, VideoPrivacy } from '@shared/models' +import { createSafeIn, getCommentSort, parseRowCountResult } from '../../../shared' import { VideoCommentTableAttributes } from './video-comment-table-attributes' export interface ListVideoCommentsOptions { diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index f0ce69501..cbd57ad8c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts @@ -1,9 +1,9 @@ import { Sequelize } from 'sequelize' import validator from 'validator' -import { createSafeIn } from '@server/models/utils' import { MUserAccountId } from '@server/types/models' import { ActorImageType } from '@shared/models' import { AbstractRunQuery } from '../../../../shared/abstract-run-query' +import { createSafeIn } from '../../../../shared' import { VideoTableAttributes } from './video-table-attributes' /** diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 7c864bf27..62f1855c7 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts @@ -2,11 +2,12 @@ import { Sequelize, Transaction } from 'sequelize' import validator from 'validator' import { exists } from '@server/helpers/custom-validators/misc' import { WEBSERVER } from '@server/initializers/constants' -import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' +import { buildSortDirectionAndField } from '@server/models/shared' import { MUserAccountId, MUserId } from '@server/types/models' +import { forceNumber } from '@shared/core-utils' import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' +import { createSafeIn, parseRowCountResult } from '../../../shared' import { AbstractRunQuery } from '../../../shared/abstract-run-query' -import { forceNumber } from '@shared/core-utils' /** * @@ -665,7 +666,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { } private buildOrder (value: string) { - const { direction, field } = buildDirectionAndField(value) + const { direction, field } = buildSortDirectionAndField(value) if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 653b9694b..cebde3755 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts @@ -4,7 +4,7 @@ import { MTag } from '@server/types/models' import { AttributesOnly } from '@shared/typescript-utils' import { VideoPrivacy, VideoState } from '../../../shared/models/videos' import { isVideoTagValid } from '../../helpers/custom-validators/videos' -import { throwIfNotValid } from '../utils' +import { throwIfNotValid } from '../shared' import { VideoModel } from './video' import { VideoTagModel } from './video-tag' diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 1cd8224c0..9247d0e2b 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -5,7 +5,7 @@ import { AttributesOnly } from '@shared/typescript-utils' import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' -import { getBlacklistSort, searchAttribute, SortType, throwIfNotValid } from '../utils' +import { getBlacklistSort, searchAttribute, throwIfNotValid } from '../shared' import { ThumbnailModel } from './thumbnail' import { VideoModel } from './video' import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' @@ -57,7 +57,7 @@ export class VideoBlacklistModel extends Model ({ diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 132c8f021..b71f5a197 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -43,8 +43,14 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' import { ActorFollowModel } from '../actor/actor-follow' import { ActorImageModel } from '../actor/actor-image' import { ServerModel } from '../server/server' -import { setAsUpdated } from '../shared' -import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' +import { + buildServerIdsFollowedBy, + buildTrigramSearchIndex, + createSimilarityAttribute, + getSort, + setAsUpdated, + throwIfNotValid +} from '../shared' import { VideoModel } from './video' import { VideoPlaylistModel } from './video-playlist' @@ -831,6 +837,6 @@ export class VideoChannelModel extends Model static doesInfohashExist (infoHash: string) { const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' - return doesExist(query, { infoHash }) + return doesExist(this.sequelize, query, { infoHash }) } static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { @@ -282,14 +281,14 @@ export class VideoFileModel extends Model 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' - return doesExist(query, { filename }) + return doesExist(this.sequelize, query, { filename }) } static async doesOwnedWebTorrentVideoFileExist (filename: string) { const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` - return doesExist(query, { filename }) + return doesExist(this.sequelize, query, { filename }) } static loadByFilename (filename: string) { diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index da6b92c7a..c040e0fda 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts @@ -22,7 +22,7 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' import { UserModel } from '../user/user' -import { getSort, searchAttribute, throwIfNotValid } from '../utils' +import { getSort, searchAttribute, throwIfNotValid } from '../shared' import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' import { VideoChannelSyncModel } from './video-channel-sync' diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index 7181b5599..48f4ed5a9 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts @@ -31,7 +31,7 @@ import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { AccountModel } from '../account/account' -import { getSort, throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../shared' import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' import { VideoPlaylistModel } from './video-playlist' diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 8bbe54c49..faf4bea78 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -21,12 +21,8 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect import { MAccountId, MChannelId } from '@server/types/models' import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' import { buildUUID, uuidToShort } from '@shared/extra-utils' +import { ActivityIconObject, PlaylistObject, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models' import { AttributesOnly } from '@shared/typescript-utils' -import { ActivityIconObject } from '../../../shared/models/activitypub/objects' -import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' -import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' -import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' -import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isVideoPlaylistDescriptionValid, @@ -53,7 +49,6 @@ import { } from '../../types/models/video/video-playlist' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { ActorModel } from '../actor/actor' -import { setAsUpdated } from '../shared' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, @@ -61,8 +56,9 @@ import { createSimilarityAttribute, getPlaylistSort, isOutdated, + setAsUpdated, throwIfNotValid -} from '../utils' +} from '../shared' import { ThumbnailModel } from './thumbnail' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' import { VideoPlaylistElementModel } from './video-playlist-element' @@ -641,7 +637,7 @@ export class VideoPlaylistModel extends Model>> { } setAsRefreshed (transaction?: Transaction) { - return setAsUpdated('video', this.id, transaction) + return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) } // --------------------------------------------------------------------------- -- 2.41.0