From b1b2d6cae075bfb34ed26d8f1da9205bead19be7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:08:25 +0000 Subject: [PATCH 1/7] feat: add upvote support for hot takes - Add UserHotTakeUpvote entity to track user upvotes - Add upvotes column to UserHotTake entity - Extend vote mutation to support hot_take entity - Add upvotes and upvoted fields to GraphQL schema - Implement voteHotTake function with upvote/remove support - Add GraphORM mappings for upvoted field - Add comprehensive test coverage for upvote functionality - Hot takes only support upvotes, not downvotes Closes #3485 Co-authored-by: Chris Bongers --- __tests__/userHotTake.ts | 225 +++++++++++++++++++++++++++ src/common/vote.ts | 47 ++++++ src/entity/user/UserHotTake.ts | 3 + src/entity/user/UserHotTakeUpvote.ts | 46 ++++++ src/entity/user/index.ts | 1 + src/graphorm/index.ts | 37 +++++ src/schema/userHotTake.ts | 2 + src/schema/users.ts | 3 + src/types.ts | 1 + 9 files changed, 365 insertions(+) create mode 100644 src/entity/user/UserHotTakeUpvote.ts diff --git a/__tests__/userHotTake.ts b/__tests__/userHotTake.ts index d29ed9a852..5956f0bc15 100644 --- a/__tests__/userHotTake.ts +++ b/__tests__/userHotTake.ts @@ -11,6 +11,7 @@ import { import { User } from '../src/entity/user/User'; import { usersFixture } from './fixture/user'; import { UserHotTake } from '../src/entity/user/UserHotTake'; +import { UserHotTakeUpvote } from '../src/entity/user/UserHotTakeUpvote'; let con: DataSource; let state: GraphQLTestingState; @@ -84,6 +85,230 @@ describe('query userHotTakes', () => { }); }); +describe('query userHotTakes with upvotes', () => { + const QUERY = ` + query UserHotTakes($userId: ID!) { + userHotTakes(userId: $userId) { + edges { + node { + id + title + upvotes + upvoted + } + } + } + } + `; + + it('should return upvotes count', async () => { + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Popular take', + position: 0, + }); + + await con.getRepository(UserHotTakeUpvote).save([ + { hotTakeId: hotTake.id, userId: '2' }, + { hotTakeId: hotTake.id, userId: '3' }, + ]); + + const res = await client.query(QUERY, { variables: { userId: '1' } }); + expect(res.data.userHotTakes.edges[0].node.upvotes).toBe(2); + }); + + it('should return upvoted as null when not logged in', async () => { + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + const res = await client.query(QUERY, { variables: { userId: '1' } }); + expect(res.data.userHotTakes.edges[0].node.upvoted).toBeNull(); + }); + + it('should return upvoted as true when user upvoted', async () => { + loggedUser = '2'; + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + await con.getRepository(UserHotTakeUpvote).save({ + hotTakeId: hotTake.id, + userId: '2', + }); + + const res = await client.query(QUERY, { variables: { userId: '1' } }); + expect(res.data.userHotTakes.edges[0].node.upvoted).toBe(true); + }); + + it('should return upvoted as false when user has not upvoted', async () => { + loggedUser = '2'; + await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + const res = await client.query(QUERY, { variables: { userId: '1' } }); + expect(res.data.userHotTakes.edges[0].node.upvoted).toBe(false); + }); +}); + +describe('mutation vote on hot take', () => { + const MUTATION = ` + mutation Vote($id: ID!, $entity: UserVoteEntity!, $vote: Int!) { + vote(id: $id, entity: $entity, vote: $vote) { + _ + } + } + `; + + it('should require authentication', async () => { + const res = await client.mutate(MUTATION, { + variables: { + id: '00000000-0000-0000-0000-000000000000', + entity: 'hot_take', + vote: 1, + }, + }); + expect(res.errors?.[0]?.extensions?.code).toBe('UNAUTHENTICATED'); + }); + + it('should upvote a hot take', async () => { + loggedUser = '2'; + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + const res = await client.mutate(MUTATION, { + variables: { + id: hotTake.id, + entity: 'hot_take', + vote: 1, + }, + }); + + expect(res.errors).toBeUndefined(); + + const upvote = await con.getRepository(UserHotTakeUpvote).findOneBy({ + hotTakeId: hotTake.id, + userId: '2', + }); + expect(upvote).not.toBeNull(); + }); + + it('should remove upvote when voting with 0', async () => { + loggedUser = '2'; + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + await con.getRepository(UserHotTakeUpvote).save({ + hotTakeId: hotTake.id, + userId: '2', + }); + + const res = await client.mutate(MUTATION, { + variables: { + id: hotTake.id, + entity: 'hot_take', + vote: 0, + }, + }); + + expect(res.errors).toBeUndefined(); + + const upvote = await con.getRepository(UserHotTakeUpvote).findOneBy({ + hotTakeId: hotTake.id, + userId: '2', + }); + expect(upvote).toBeNull(); + }); + + it('should not allow downvoting hot takes', async () => { + loggedUser = '2'; + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + const res = await client.mutate(MUTATION, { + variables: { + id: hotTake.id, + entity: 'hot_take', + vote: -1, + }, + }); + + expect(res.errors?.[0]?.message).toBe( + 'Hot takes do not support downvotes', + ); + }); + + it('should return error for non-existent hot take', async () => { + loggedUser = '2'; + const res = await client.mutate(MUTATION, { + variables: { + id: '00000000-0000-0000-0000-000000000000', + entity: 'hot_take', + vote: 1, + }, + }); + + expect(res.errors).toBeDefined(); + }); + + it('should allow upvoting same hot take only once', async () => { + loggedUser = '2'; + const hotTake = await con.getRepository(UserHotTake).save({ + userId: '1', + emoji: '🔥', + title: 'Take', + position: 0, + }); + + await client.mutate(MUTATION, { + variables: { + id: hotTake.id, + entity: 'hot_take', + vote: 1, + }, + }); + + const res = await client.mutate(MUTATION, { + variables: { + id: hotTake.id, + entity: 'hot_take', + vote: 1, + }, + }); + + expect(res.errors).toBeUndefined(); + + const upvotes = await con.getRepository(UserHotTakeUpvote).findBy({ + hotTakeId: hotTake.id, + userId: '2', + }); + expect(upvotes).toHaveLength(1); + }); +}); + describe('mutation addUserHotTake', () => { const MUTATION = ` mutation AddUserHotTake($input: AddUserHotTakeInput!) { diff --git a/src/common/vote.ts b/src/common/vote.ts index 706057749f..bcf93ec312 100644 --- a/src/common/vote.ts +++ b/src/common/vote.ts @@ -9,6 +9,8 @@ import { GQLEmptyResponse } from '../schema/common'; import { ensureSourcePermissions } from '../schema/sources'; import { AuthContext } from '../Context'; import { UserComment } from '../entity/user/UserComment'; +import { UserHotTake } from '../entity/user/UserHotTake'; +import { UserHotTakeUpvote } from '../entity/user/UserHotTakeUpvote'; import { UserVote } from '../types'; type UserVoteProps = { @@ -154,3 +156,48 @@ export const voteComment = async ({ return { _: true }; }; + +export const voteHotTake = async ({ + ctx, + id, + vote, +}: UserVoteProps): Promise => { + try { + validateVoteType({ vote }); + + const hotTake = await ctx.con + .getRepository(UserHotTake) + .findOneByOrFail({ id }); + + const userHotTakeUpvoteRepo = ctx.con.getRepository(UserHotTakeUpvote); + + switch (vote) { + case UserVote.Up: + await userHotTakeUpvoteRepo.save({ + hotTakeId: id, + userId: ctx.userId, + }); + break; + case UserVote.None: + await userHotTakeUpvoteRepo.delete({ + hotTakeId: id, + userId: ctx.userId, + }); + break; + case UserVote.Down: + throw new ValidationError('Hot takes do not support downvotes'); + default: + throw new ValidationError('Unsupported vote type'); + } + } catch (originalError) { + const err = originalError as TypeORMQueryFailedError; + + if (err?.code === TypeOrmError.FOREIGN_KEY) { + throw new NotFoundError('Hot take or user not found'); + } + + throw err; + } + + return { _: true }; +}; diff --git a/src/entity/user/UserHotTake.ts b/src/entity/user/UserHotTake.ts index 872c33d515..b3fab490dd 100644 --- a/src/entity/user/UserHotTake.ts +++ b/src/entity/user/UserHotTake.ts @@ -31,6 +31,9 @@ export class UserHotTake { @Column({ type: 'integer' }) position: number; + @Column({ type: 'integer', default: 0 }) + upvotes: number; + @Column({ type: 'timestamp', default: () => 'now()' }) createdAt: Date; diff --git a/src/entity/user/UserHotTakeUpvote.ts b/src/entity/user/UserHotTakeUpvote.ts new file mode 100644 index 0000000000..42e52752f6 --- /dev/null +++ b/src/entity/user/UserHotTakeUpvote.ts @@ -0,0 +1,46 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; +import type { User } from './User'; +import type { UserHotTake } from './UserHotTake'; + +@Entity() +@Index(['hotTakeId', 'userId'], { unique: true }) +@Index(['userId', 'createdAt']) +@Index(['hotTakeId', 'createdAt']) +export class UserHotTakeUpvote { + @PrimaryColumn({ type: 'uuid' }) + hotTakeId: string; + + @PrimaryColumn({ type: 'text' }) + userId: string; + + @CreateDateColumn() + createdAt: Date; + + @ManyToOne('UserHotTake', { + lazy: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'hotTakeId', + foreignKeyConstraintName: 'FK_user_hot_take_upvote_hot_take_id', + }) + hotTake: Promise; + + @ManyToOne('User', { + lazy: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'userId', + foreignKeyConstraintName: 'FK_user_hot_take_upvote_user_id', + }) + user: Promise; +} diff --git a/src/entity/user/index.ts b/src/entity/user/index.ts index 99c0b8001b..834cd04e51 100644 --- a/src/entity/user/index.ts +++ b/src/entity/user/index.ts @@ -9,6 +9,7 @@ export * from './UserStats'; export * from './UserTopReader'; export * from './UserStack'; export * from './UserHotTake'; +export * from './UserHotTakeUpvote'; export * from './UserWorkspacePhoto'; export * from './UserGear'; export * from './UserTool'; diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index 177a613b4f..8296a95b25 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -107,6 +107,31 @@ const existsByUserAndPost = END`; }; +const existsByUserAndHotTake = + (entity: string, build?: (queryBuilder: QueryBuilder) => QueryBuilder) => + (ctx: Context, alias: string, qb: QueryBuilder): string => { + let query = qb + .select('1') + .from(entity, 'a') + .where(`a."userId" = :userId`, { userId: ctx.userId }) + .andWhere(`a."hotTakeId" = ${alias}.id`) + .limit(1); + + if (typeof build === 'function') { + query = build(query); + } + + return /*sql*/ `CASE + WHEN + ${query.getQuery()} + IS NOT NULL + THEN + TRUE + ELSE + FALSE + END`; + }; + const nullIfNotLoggedIn = (value: T, ctx: Context): T | null => ctx.userId ? value : null; @@ -2178,6 +2203,18 @@ const obj = new GraphORM({ }, }, }, + UserHotTake: { + requiredColumns: ['id', 'userId'], + fields: { + upvoted: { + select: existsByUserAndHotTake('UserHotTakeUpvote'), + transform: nullIfNotLoggedIn, + }, + createdAt: { + transform: transformDate, + }, + }, + }, }); export default obj; diff --git a/src/schema/userHotTake.ts b/src/schema/userHotTake.ts index d33cbff617..7c70a63415 100644 --- a/src/schema/userHotTake.ts +++ b/src/schema/userHotTake.ts @@ -34,6 +34,8 @@ export const typeDefs = /* GraphQL */ ` title: String! subtitle: String position: Int! + upvotes: Int! + upvoted: Boolean createdAt: DateTime! } diff --git a/src/schema/users.ts b/src/schema/users.ts index 54f558f516..3b44658ab6 100644 --- a/src/schema/users.ts +++ b/src/schema/users.ts @@ -87,6 +87,7 @@ import { validateWorkEmailDomain, voteComment, votePost, + voteHotTake, } from '../common'; import { getSearchQuery, GQLEmptyResponse, processSearchQuery } from './common'; import { ActiveView } from '../entity/ActiveView'; @@ -3321,6 +3322,8 @@ export const resolvers: IResolvers = traceResolvers< return votePost({ ctx, id, vote }); case UserVoteEntity.Comment: return voteComment({ ctx, id, vote }); + case UserVoteEntity.HotTake: + return voteHotTake({ ctx, id, vote }); default: throw new ValidationError('Unsupported vote entity'); } diff --git a/src/types.ts b/src/types.ts index a5dfe2886c..1c4f6d698f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -169,6 +169,7 @@ export enum UserVote { export enum UserVoteEntity { Comment = 'comment', Post = 'post', + HotTake = 'hot_take', } export const maxFeedsPerUser = 20; From 781e6463dfba38ffe6d393b177893ce39355b671 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 11:00:33 +0200 Subject: [PATCH 2/7] fix: update upvotes count when voting on hot takes The upvotes column on UserHotTake was not being updated when users voted. Now uses a transaction to atomically: - Check if upvote already exists - Insert/delete the upvote record - Increment/decrement the upvotes count Co-Authored-By: Claude Opus 4.5 --- src/common/vote.ts | 37 +- .../1769156534090-AddUserHotTakeUpvotes.ts | 486 ++++++++++++++++++ 2 files changed, 507 insertions(+), 16 deletions(-) create mode 100644 src/migration/1769156534090-AddUserHotTakeUpvotes.ts diff --git a/src/common/vote.ts b/src/common/vote.ts index bcf93ec312..75dec7f2b7 100644 --- a/src/common/vote.ts +++ b/src/common/vote.ts @@ -165,30 +165,35 @@ export const voteHotTake = async ({ try { validateVoteType({ vote }); - const hotTake = await ctx.con - .getRepository(UserHotTake) - .findOneByOrFail({ id }); + if (vote === UserVote.Down) { + throw new ValidationError('Hot takes do not support downvotes'); + } - const userHotTakeUpvoteRepo = ctx.con.getRepository(UserHotTakeUpvote); + await ctx.con.transaction(async (manager) => { + const hotTakeRepo = manager.getRepository(UserHotTake); + const upvoteRepo = manager.getRepository(UserHotTakeUpvote); - switch (vote) { - case UserVote.Up: - await userHotTakeUpvoteRepo.save({ + await hotTakeRepo.findOneByOrFail({ id }); + + const existingUpvote = await upvoteRepo.findOneBy({ + hotTakeId: id, + userId: ctx.userId, + }); + + if (vote === UserVote.Up && !existingUpvote) { + await upvoteRepo.insert({ hotTakeId: id, userId: ctx.userId, }); - break; - case UserVote.None: - await userHotTakeUpvoteRepo.delete({ + await hotTakeRepo.increment({ id }, 'upvotes', 1); + } else if (vote === UserVote.None && existingUpvote) { + await upvoteRepo.delete({ hotTakeId: id, userId: ctx.userId, }); - break; - case UserVote.Down: - throw new ValidationError('Hot takes do not support downvotes'); - default: - throw new ValidationError('Unsupported vote type'); - } + await hotTakeRepo.decrement({ id }, 'upvotes', 1); + } + }); } catch (originalError) { const err = originalError as TypeORMQueryFailedError; diff --git a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts new file mode 100644 index 0000000000..72939ac97d --- /dev/null +++ b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts @@ -0,0 +1,486 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddUserHotTakeUpvotes1769156534090 implements MigrationInterface { + name = 'AddUserHotTakeUpvotes1769156534090' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats"`); + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","popular_video_source","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "popular_video_source"`); + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","source_tag_view","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "source_tag_view"`); + await queryRunner.query(`ALTER TABLE "view" DROP CONSTRAINT "FK_82db04e5b5686aec67abf4577e9"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_83c8ae07417cd6de65b8b994587"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_f1bb0e9a2279673a76520d2adc5"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_a51326b93176f2b2ebf3eda9fef"`); + await queryRunner.query(`ALTER TABLE "post_keyword" DROP CONSTRAINT "FK_88d97436b07e1462d5a7877dcb3"`); + await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_231b13306df1d5046ffa7c4568b"`); + await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_a6977adf724e068f6faae2914da"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_d82dbf00f15d651edf917c8167f"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_f1305337f54e8211d5a84da0cc5"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_cb2ce95b74f2ee583447362d508"`); + await queryRunner.query(`ALTER TABLE "user_transaction" DROP CONSTRAINT "FK_1422cc85c1642eed91e947cf877"`); + await queryRunner.query(`ALTER TABLE "user_personalized_digest" DROP CONSTRAINT "FK_user_personalized_digest_user"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_user_locationId"`); + await queryRunner.query(`ALTER TABLE "alerts" DROP CONSTRAINT "FK_f2678f7b11e5128abbbc4511906"`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_b0df46b7939819e8bc14cf9f45c"`); + await queryRunner.query(`ALTER TABLE "dev_card" DROP CONSTRAINT "FK_70a77f197a0f92324256c983fc6"`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_01d5bc49fc5802c4e067d7ba4c9"`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_c46745c935040ef6188c9cbf013"`); + await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_b08384d9c394e68429a9eea4df7"`); + await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_a4ead2fdceb9998b4dc4d01935b"`); + await queryRunner.query(`ALTER TABLE "feed_tag" DROP CONSTRAINT "FK_8c6d05462bc68459e00f165d51c"`); + await queryRunner.query(`ALTER TABLE "post_report" DROP CONSTRAINT "FK_d1d5a13218f895570f4d7ad5897"`); + await queryRunner.query(`ALTER TABLE "post_tag" DROP CONSTRAINT "FK_f3c0b831daae119196482c99379"`); + await queryRunner.query(`ALTER TABLE "settings" DROP CONSTRAINT "FK_9175e059b0a720536f7726a88c7"`); + await queryRunner.query(`ALTER TABLE "source_display" DROP CONSTRAINT "FK_ad5e759962cd86ff322cb480d09"`); + await queryRunner.query(`ALTER TABLE "source_feed" DROP CONSTRAINT "FK_4f708e773c1df9c288180fbb555"`); + await queryRunner.query(`ALTER TABLE "user_company" DROP CONSTRAINT "FK_9c279d6cf291c858efa8a6b143f"`); + await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_organization_dataset_location_locationId"`); + await queryRunner.query(`ALTER TABLE "question" DROP CONSTRAINT "FK_question_screening_opportunity_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_82db04e5b5686aec67abf4577e"`); + await queryRunner.query(`DROP INDEX "public"."IDX_339031b134e88d3096bfaf928c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9cb4aa5be0760354054764eefb"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e74e00c9de47272d1a9ea327ab"`); + await queryRunner.query(`DROP INDEX "public"."IDX_post_url"`); + await queryRunner.query(`DROP INDEX "public"."IDX_post_canonicalUrl"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_transaction_createdAt_desc"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_transaction_status_updated_at"`); + await queryRunner.query(`DROP INDEX "public"."IDX_7e1d93d646c13a3a0a2c7e2d5a"`); + await queryRunner.query(`DROP INDEX "public"."user_idx_lowerusername_username"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_gin_username"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_gin_name"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_reputation"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_cioRegistered"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b2e3f7568dafa9e86ae0391011"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_streak_currentstreak_userid"`); + await queryRunner.query(`DROP INDEX "public"."idx_user_streak_totalstreak_userid"`); + await queryRunner.query(`DROP INDEX "public"."IDX_85dda8e9982027f27696273a20"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b0df46b7939819e8bc14cf9f45"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a56238722b511b0be1ce2ef260"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b08384d9c394e68429a9eea4df"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a4ead2fdceb9998b4dc4d01935"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f3c0b831daae119196482c9937"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cc6fec511089f9ea5017b15d77"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c41a183d0b219907f07efa3e11"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9caaea7864887299e2cb4ef355"`); + await queryRunner.query(`ALTER TABLE "dataset_location" DROP CONSTRAINT "CHK_dataset_location_country_or_continent"`); + await queryRunner.query(`CREATE TABLE "user_hot_take_upvote" ("hotTakeId" uuid NOT NULL, "userId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_0fb6851f42e1d3f4138e3e4b8ca" PRIMARY KEY ("hotTakeId", "userId"))`); + await queryRunner.query(`CREATE INDEX "IDX_902a36de7917e47ab8839ae46f" ON "user_hot_take_upvote" ("hotTakeId", "createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_cc451486cbf598411e27264102" ON "user_hot_take_upvote" ("userId", "createdAt") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0fb6851f42e1d3f4138e3e4b8c" ON "user_hot_take_upvote" ("hotTakeId", "userId") `); + await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "upvotes" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "user_integration" ALTER COLUMN "meta" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_source_integration" ALTER COLUMN "channelIds" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberPostingRank" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberInviteRank" DROP NOT NULL`); + await queryRunner.query(`DROP INDEX "public"."IDX_status_value"`); + await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_value_occurrences"`); + await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_occ_value"`); + await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status"`); + await queryRunner.query(`ALTER TABLE "keyword" DROP COLUMN "status"`); + await queryRunner.query(`ALTER TABLE "keyword" ADD "status" text NOT NULL DEFAULT 'pending'`); + await queryRunner.query(`DROP INDEX "public"."IDX_content_preference_reference_user_id"`); + await queryRunner.query(`ALTER TABLE "content_preference" DROP COLUMN "referenceUserId"`); + await queryRunner.query(`ALTER TABLE "content_preference" ADD "referenceUserId" character varying`); + await queryRunner.query(`ALTER TABLE "comment" ALTER COLUMN "contentHtml" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "feed" DROP CONSTRAINT "FK_70952a3f1b3717e7021a439edda"`); + await queryRunner.query(`DROP INDEX "public"."IDX_feed_id_user_id"`); + await queryRunner.query(`ALTER TABLE "feed" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "feed" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_hot_take_user_id"`); + await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_ff0f49b797aca629f81cef47610"`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_ff0f49b797aca629f81cef47610" UNIQUE ("defaultFeedId")`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "FK_c025478b45e60017ed10c77f99c"`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type")`); + await queryRunner.query(`ALTER TABLE "user_action" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type", "userId")`); + await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`); + await queryRunner.query(`ALTER TABLE "user_stack" DROP CONSTRAINT "FK_user_stack_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_stack_user_id"`); + await queryRunner.query(`ALTER TABLE "user_stack" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_stack" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP CONSTRAINT "FK_user_workspace_photo_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_workspace_photo_user_id"`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_gear" DROP CONSTRAINT "FK_user_gear_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_gear_user_id"`); + await queryRunner.query(`ALTER TABLE "user_gear" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_gear" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_tool" DROP CONSTRAINT "FK_user_tool_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_tool_user_id"`); + await queryRunner.query(`ALTER TABLE "user_tool" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_tool" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bookmark_userId_createdAt"`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId")`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId", "userId")`); + await queryRunner.query(`ALTER TABLE "bookmark_list" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "bookmark_list" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "submission" ALTER COLUMN "reason" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "FK_c8721bd56ae600308745ad49744"`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType")`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD "userId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType", "userId")`); + await queryRunner.query(`ALTER TABLE "user_candidate_preference" ALTER COLUMN "locationType" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "dataset_location" ADD CONSTRAINT "UQ_3d2d01d9c20653dd58e6741666c" UNIQUE ("externalId")`); + await queryRunner.query(`CREATE INDEX "IDX_19da087dd68a0bc5e5302ca9a5" ON "view" ("postId") `); + await queryRunner.query(`CREATE INDEX "IDX_c6af9853ff6a60d2e80dd8b3af" ON "view" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_10dff5dbc360f0d47cada787c7" ON "view" ("timestamp") `); + await queryRunner.query(`CREATE INDEX "IDX_c63ec25b983985f2fee951afcc" ON "view" ("postId", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_post_awards" ON "post" ("awards") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2d4cb7f2ff3bcc12f0639d8f86" ON "post" ("url") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_02634e624fee03af415a7597ed" ON "post" ("canonicalUrl") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2d4cb7f2ff3bcc12f0639d8f86" ON "post" ("url") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_02634e624fee03af415a7597ed" ON "post" ("canonicalUrl") `); + await queryRunner.query(`CREATE INDEX "IDX_b499447822de3f24ad355e19b8" ON "post" ("type") `); + await queryRunner.query(`CREATE INDEX "IDX_keyword_status" ON "keyword" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_keyword_status_value_occurrences" ON "keyword" ("status", "value", "occurrences") `); + await queryRunner.query(`CREATE INDEX "IDX_status_value" ON "keyword" ("status", "value") `); + await queryRunner.query(`CREATE INDEX "IDX_content_preference_reference_user_id" ON "content_preference" ("referenceUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_comment_awards" ON "comment" ("awards") `); + await queryRunner.query(`CREATE INDEX "IDX_70952a3f1b3717e7021a439edd" ON "feed" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_feed_id_user_id" ON "feed" ("id", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_user_id" ON "user_hot_take" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_c025478b45e60017ed10c77f99" ON "user_action" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_stack_user_id" ON "user_stack" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_workspace_photo_user_id" ON "user_workspace_photo" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_gear_user_id" ON "user_gear" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_tool_user_id" ON "user_tool" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_f2678f7b11e5128abbbc451190" ON "alerts" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_ee4a9b84b65399f9a6581d9a9a" ON "bookmark" ("postId") `); + await queryRunner.query(`CREATE INDEX "IDX_ff105847cfef10dd4af15b52a9" ON "bookmark_list" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_a9b3d81d71cd6c2cd82249f689" ON "feed_source" ("feedId") `); + await queryRunner.query(`CREATE INDEX "IDX_79143dac6de88d4ba0f0ecfa0d" ON "feed_source" ("sourceId") `); + await queryRunner.query(`CREATE INDEX "IDX_444c1b4f6cd7b632277f557935" ON "post_tag" ("postId") `); + await queryRunner.query(`CREATE INDEX "IDX_a72fa6b0a0b9ea438fbef00cb3" ON "source_display" ("userId", "enabled") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bf5818e5de16bc943005a50166" ON "source_display" ("sourceId", "userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f83844a38b17aa07772469cb08" ON "source_feed" ("sourceId", "feed") `); + await queryRunner.query(`CREATE INDEX "IDX_6d32bd7a8976dac90c4e11b340" ON "tag_segment" ("segment") `); + await queryRunner.query(`CREATE INDEX "IDX_65a9ca0600dbc72c6ff76501a6" ON "notification_preference" ("type") `); + await queryRunner.query(`ALTER TABLE "view" ADD CONSTRAINT "FK_19da087dd68a0bc5e5302ca9a59" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_0b78981ffc8817ce54da9180a8d" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_1609023c77409ed0c4388ec240e" FOREIGN KEY ("scoutId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_keyword" ADD CONSTRAINT "FK_b96fa78416c5366186a32b9dd45" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_cea0cfe859c4cb4e35da879e111" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_94a85bb16d24033a2afdd5df060" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_c0354a9a009d3bb45a08655ce3b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_transaction" ADD CONSTRAINT "FK_bb9c8cc52dbf33107f613fb8302" FOREIGN KEY ("receiverId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed" ADD CONSTRAINT "FK_70952a3f1b3717e7021a439edda" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_hot_take" ADD CONSTRAINT "FK_user_hot_take_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" ADD CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" FOREIGN KEY ("hotTakeId") REFERENCES "user_hot_take"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" ADD CONSTRAINT "FK_user_hot_take_upvote_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_ff0f49b797aca629f81cef47610" FOREIGN KEY ("defaultFeedId") REFERENCES "feed"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_user_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "FK_c025478b45e60017ed10c77f99c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_stack" ADD CONSTRAINT "FK_user_stack_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD CONSTRAINT "FK_user_workspace_photo_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_gear" ADD CONSTRAINT "FK_user_gear_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_tool" ADD CONSTRAINT "FK_user_tool_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_ee4a9b84b65399f9a6581d9a9a5" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "bookmark_list" ADD CONSTRAINT "FK_ff105847cfef10dd4af15b52a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "dev_card" ADD CONSTRAINT "FK_338150b6c9b6d9b788d285d1c95" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_eaa6a8fb25235695fb7f99af859" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_67f0b1889dd335ea2c196146f82" FOREIGN KEY ("advancedSettingsId") REFERENCES "advanced_settings"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_a9b3d81d71cd6c2cd82249f6895" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_79143dac6de88d4ba0f0ecfa0d9" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_tag" ADD CONSTRAINT "FK_e875186f4a0f01b72cc3e79eaee" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_report" ADD CONSTRAINT "FK_44b0e753044952524eb47ac6f40" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_tag" ADD CONSTRAINT "FK_444c1b4f6cd7b632277f5579354" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "source_display" ADD CONSTRAINT "FK_b65c592c197135747cab64e986a" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "source_feed" ADD CONSTRAINT "FK_725e606248d79f0f10d0fe730c0" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "FK_c8721bd56ae600308745ad49744" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_organization_dataset_location_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "question" ADD CONSTRAINT "FK_question_feedback_opportunity_id" FOREIGN KEY ("opportunityId") REFERENCES "opportunity"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_analytics_history" ADD CONSTRAINT "FK_6bb82232882b9a8bfa54a034c5f" FOREIGN KEY ("id") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_analytics" ADD CONSTRAINT "FK_421771f55c623cd4f6103828196" FOREIGN KEY ("id") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`CREATE MATERIALIZED VIEW "source_tag_view" AS SELECT "s"."id" as sourceId, "pk"."keyword" AS tag, count("pk"."keyword") AS count FROM "public"."source" "s" INNER JOIN "public"."post" "p" ON "p"."sourceId" = "s"."id" AND "p"."createdAt" > :time INNER JOIN "public"."post_keyword" "pk" ON "pk"."postId" = "p"."id" AND "pk"."status" = :status WHERE ("s"."active" = :orm_param_0 AND "s"."private" = :orm_param_1) GROUP BY sourceId, tag`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","source_tag_view","SELECT \"s\".\"id\" as sourceId, \"pk\".\"keyword\" AS tag, count(\"pk\".\"keyword\") AS count FROM \"public\".\"source\" \"s\" INNER JOIN \"public\".\"post\" \"p\" ON \"p\".\"sourceId\" = \"s\".\"id\" AND \"p\".\"createdAt\" > :time INNER JOIN \"public\".\"post_keyword\" \"pk\" ON \"pk\".\"postId\" = \"p\".\"id\" AND \"pk\".\"status\" = :status WHERE (\"s\".\"active\" = :orm_param_0 AND \"s\".\"private\" = :orm_param_1) GROUP BY sourceId, tag"]); + await queryRunner.query(`CREATE MATERIALIZED VIEW "popular_video_source" AS SELECT "sourceId", avg(r) r, count(*) posts FROM "public"."popular_video_post" "base" GROUP BY "sourceId" HAVING count(*) > 5 ORDER BY r DESC`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","popular_video_source","SELECT \"sourceId\", avg(r) r, count(*) posts FROM \"public\".\"popular_video_post\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 5 ORDER BY r DESC"]); + await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0) + FROM "user" + WHERE "referralId" = u."id" + ) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0) + FROM "post" p + WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") + AND p."visible" = TRUE + AND p."deleted" = FALSE + ) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0) + FROM "post" p + WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") + AND p."visible" = TRUE + AND p."deleted" = FALSE + ) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0) + FROM "comment" c + WHERE c."userId" = u."id" + ) AS "commentUpvotes", (SELECT COALESCE(COUNT(*), 0) + FROM "user_top_reader" utp + WHERE utp."userId" = u."id" + ) AS "topReaderBadges" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != :ghostId`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user_top_reader\" utp\n WHERE utp.\"userId\" = u.\"id\"\n ) AS \"topReaderBadges\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != :ghostId"]); + await queryRunner.query(`CREATE INDEX "IDX_sourceTag_sourceId" ON "source_tag_view" ("sourceId") `); + await queryRunner.query(`CREATE INDEX "IDX_sourceTag_tag" ON "source_tag_view" ("tag") `); + await queryRunner.query(`CREATE INDEX "IDX_user_stats_id" ON "user_stats" ("id") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_user_stats_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_sourceTag_tag"`); + await queryRunner.query(`DROP INDEX "public"."IDX_sourceTag_sourceId"`); + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats"`); + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","popular_video_source","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "popular_video_source"`); + await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","source_tag_view","public"]); + await queryRunner.query(`DROP MATERIALIZED VIEW "source_tag_view"`); + await queryRunner.query(`ALTER TABLE "post_analytics" DROP CONSTRAINT "FK_421771f55c623cd4f6103828196"`); + await queryRunner.query(`ALTER TABLE "post_analytics_history" DROP CONSTRAINT "FK_6bb82232882b9a8bfa54a034c5f"`); + await queryRunner.query(`ALTER TABLE "question" DROP CONSTRAINT "FK_question_feedback_opportunity_id"`); + await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_organization_dataset_location_locationId"`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "FK_c8721bd56ae600308745ad49744"`); + await queryRunner.query(`ALTER TABLE "source_feed" DROP CONSTRAINT "FK_725e606248d79f0f10d0fe730c0"`); + await queryRunner.query(`ALTER TABLE "source_display" DROP CONSTRAINT "FK_b65c592c197135747cab64e986a"`); + await queryRunner.query(`ALTER TABLE "post_tag" DROP CONSTRAINT "FK_444c1b4f6cd7b632277f5579354"`); + await queryRunner.query(`ALTER TABLE "post_report" DROP CONSTRAINT "FK_44b0e753044952524eb47ac6f40"`); + await queryRunner.query(`ALTER TABLE "feed_tag" DROP CONSTRAINT "FK_e875186f4a0f01b72cc3e79eaee"`); + await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_79143dac6de88d4ba0f0ecfa0d9"`); + await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_a9b3d81d71cd6c2cd82249f6895"`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_67f0b1889dd335ea2c196146f82"`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_eaa6a8fb25235695fb7f99af859"`); + await queryRunner.query(`ALTER TABLE "dev_card" DROP CONSTRAINT "FK_338150b6c9b6d9b788d285d1c95"`); + await queryRunner.query(`ALTER TABLE "bookmark_list" DROP CONSTRAINT "FK_ff105847cfef10dd4af15b52a9a"`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b"`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_ee4a9b84b65399f9a6581d9a9a5"`); + await queryRunner.query(`ALTER TABLE "user_tool" DROP CONSTRAINT "FK_user_tool_user_id"`); + await queryRunner.query(`ALTER TABLE "user_gear" DROP CONSTRAINT "FK_user_gear_user_id"`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP CONSTRAINT "FK_user_workspace_photo_user_id"`); + await queryRunner.query(`ALTER TABLE "user_stack" DROP CONSTRAINT "FK_user_stack_user_id"`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "FK_c025478b45e60017ed10c77f99c"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_user_locationId"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_ff0f49b797aca629f81cef47610"`); + await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_user_id"`); + await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_hot_take_id"`); + await queryRunner.query(`ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_user_id"`); + await queryRunner.query(`ALTER TABLE "feed" DROP CONSTRAINT "FK_70952a3f1b3717e7021a439edda"`); + await queryRunner.query(`ALTER TABLE "user_transaction" DROP CONSTRAINT "FK_bb9c8cc52dbf33107f613fb8302"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_e3aebe2bd1c53467a07109be596"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_c0354a9a009d3bb45a08655ce3b"`); + await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_94a85bb16d24033a2afdd5df060"`); + await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_cea0cfe859c4cb4e35da879e111"`); + await queryRunner.query(`ALTER TABLE "post_keyword" DROP CONSTRAINT "FK_b96fa78416c5366186a32b9dd45"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_1609023c77409ed0c4388ec240e"`); + await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_0b78981ffc8817ce54da9180a8d"`); + await queryRunner.query(`ALTER TABLE "view" DROP CONSTRAINT "FK_19da087dd68a0bc5e5302ca9a59"`); + await queryRunner.query(`DROP INDEX "public"."IDX_65a9ca0600dbc72c6ff76501a6"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6d32bd7a8976dac90c4e11b340"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f83844a38b17aa07772469cb08"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bf5818e5de16bc943005a50166"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a72fa6b0a0b9ea438fbef00cb3"`); + await queryRunner.query(`DROP INDEX "public"."IDX_444c1b4f6cd7b632277f557935"`); + await queryRunner.query(`DROP INDEX "public"."IDX_79143dac6de88d4ba0f0ecfa0d"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a9b3d81d71cd6c2cd82249f689"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ff105847cfef10dd4af15b52a9"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ee4a9b84b65399f9a6581d9a9a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f2678f7b11e5128abbbc451190"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_tool_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_gear_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_workspace_photo_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_stack_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c025478b45e60017ed10c77f99"`); + await queryRunner.query(`DROP INDEX "public"."IDX_user_hot_take_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_feed_id_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_70952a3f1b3717e7021a439edd"`); + await queryRunner.query(`DROP INDEX "public"."IDX_comment_awards"`); + await queryRunner.query(`DROP INDEX "public"."IDX_content_preference_reference_user_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_status_value"`); + await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_value_occurrences"`); + await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b499447822de3f24ad355e19b8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_02634e624fee03af415a7597ed"`); + await queryRunner.query(`DROP INDEX "public"."IDX_2d4cb7f2ff3bcc12f0639d8f86"`); + await queryRunner.query(`DROP INDEX "public"."IDX_02634e624fee03af415a7597ed"`); + await queryRunner.query(`DROP INDEX "public"."IDX_2d4cb7f2ff3bcc12f0639d8f86"`); + await queryRunner.query(`DROP INDEX "public"."IDX_post_awards"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c63ec25b983985f2fee951afcc"`); + await queryRunner.query(`DROP INDEX "public"."IDX_10dff5dbc360f0d47cada787c7"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c6af9853ff6a60d2e80dd8b3af"`); + await queryRunner.query(`DROP INDEX "public"."IDX_19da087dd68a0bc5e5302ca9a5"`); + await queryRunner.query(`ALTER TABLE "dataset_location" DROP CONSTRAINT "UQ_3d2d01d9c20653dd58e6741666c"`); + await queryRunner.query(`ALTER TABLE "user_candidate_preference" ALTER COLUMN "locationType" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType")`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD "userId" text NOT NULL`); + await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "userId", "notificationType")`); + await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "FK_c8721bd56ae600308745ad49744" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "submission" ALTER COLUMN "reason" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "bookmark_list" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "bookmark_list" ADD "userId" text NOT NULL`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId")`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD "userId" text NOT NULL`); + await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId", "userId")`); + await queryRunner.query(`CREATE INDEX "IDX_bookmark_userId_createdAt" ON "bookmark" ("createdAt", "userId") `); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_tool" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_tool" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_user_tool_user_id" ON "user_tool" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_tool" ADD CONSTRAINT "FK_user_tool_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_gear" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_gear" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_user_gear_user_id" ON "user_gear" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_gear" ADD CONSTRAINT "FK_user_gear_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_user_workspace_photo_user_id" ON "user_workspace_photo" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD CONSTRAINT "FK_user_workspace_photo_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_stack" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_stack" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_user_stack_user_id" ON "user_stack" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_stack" ADD CONSTRAINT "FK_user_stack_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type")`); + await queryRunner.query(`ALTER TABLE "user_action" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD "userId" text NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("userId", "type")`); + await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "FK_c025478b45e60017ed10c77f99c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_ff0f49b797aca629f81cef47610"`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_ff0f49b797aca629f81cef47610" FOREIGN KEY ("defaultFeedId") REFERENCES "feed"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_user_id" ON "user_hot_take" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_hot_take" ADD CONSTRAINT "FK_user_hot_take_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed" DROP COLUMN "userId"`); + await queryRunner.query(`ALTER TABLE "feed" ADD "userId" text NOT NULL`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_feed_id_user_id" ON "feed" ("id", "userId") `); + await queryRunner.query(`ALTER TABLE "feed" ADD CONSTRAINT "FK_70952a3f1b3717e7021a439edda" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ALTER COLUMN "contentHtml" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "content_preference" DROP COLUMN "referenceUserId"`); + await queryRunner.query(`ALTER TABLE "content_preference" ADD "referenceUserId" text`); + await queryRunner.query(`CREATE INDEX "IDX_content_preference_reference_user_id" ON "content_preference" ("referenceUserId") `); + await queryRunner.query(`ALTER TABLE "keyword" DROP COLUMN "status"`); + await queryRunner.query(`ALTER TABLE "keyword" ADD "status" character varying NOT NULL DEFAULT 'pending'`); + await queryRunner.query(`CREATE INDEX "IDX_keyword_status" ON "keyword" ("status") `); + await queryRunner.query(`CREATE INDEX "IDX_keyword_status_occ_value" ON "keyword" ("occurrences", "status", "value") `); + await queryRunner.query(`CREATE INDEX "IDX_keyword_status_value_occurrences" ON "keyword" ("occurrences", "status", "value") `); + await queryRunner.query(`CREATE INDEX "IDX_status_value" ON "keyword" ("status", "value") `); + await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberInviteRank" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberPostingRank" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_source_integration" ALTER COLUMN "channelIds" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_integration" ALTER COLUMN "meta" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "upvotes"`); + await queryRunner.query(`DROP INDEX "public"."IDX_0fb6851f42e1d3f4138e3e4b8c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cc451486cbf598411e27264102"`); + await queryRunner.query(`DROP INDEX "public"."IDX_902a36de7917e47ab8839ae46f"`); + await queryRunner.query(`DROP TABLE "user_hot_take_upvote"`); + await queryRunner.query(`ALTER TABLE "dataset_location" ADD CONSTRAINT "CHK_dataset_location_country_or_continent" CHECK (((country IS NOT NULL) OR (continent IS NOT NULL)))`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9caaea7864887299e2cb4ef355" ON "source_feed" ("feed", "sourceId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c41a183d0b219907f07efa3e11" ON "source_display" ("sourceId", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_cc6fec511089f9ea5017b15d77" ON "source_display" ("enabled", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_f3c0b831daae119196482c9937" ON "post_tag" ("postId") `); + await queryRunner.query(`CREATE INDEX "IDX_a4ead2fdceb9998b4dc4d01935" ON "feed_source" ("sourceId") `); + await queryRunner.query(`CREATE INDEX "IDX_b08384d9c394e68429a9eea4df" ON "feed_source" ("feedId") `); + await queryRunner.query(`CREATE INDEX "IDX_a56238722b511b0be1ce2ef260" ON "bookmark" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_b0df46b7939819e8bc14cf9f45" ON "bookmark" ("postId") `); + await queryRunner.query(`CREATE INDEX "IDX_85dda8e9982027f27696273a20" ON "alerts" ("userId") `); + await queryRunner.query(`CREATE INDEX "idx_user_streak_totalstreak_userid" ON "user_streak" ("totalStreak", "userId") `); + await queryRunner.query(`CREATE INDEX "idx_user_streak_currentstreak_userid" ON "user_streak" ("currentStreak", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_b2e3f7568dafa9e86ae0391011" ON "user_action" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_user_cioRegistered" ON "user" ("cioRegistered") `); + await queryRunner.query(`CREATE INDEX "idx_user_reputation" ON "user" ("reputation") `); + await queryRunner.query(`CREATE INDEX "idx_user_gin_name" ON "user" ("name") `); + await queryRunner.query(`CREATE INDEX "idx_user_gin_username" ON "user" ("username") `); + await queryRunner.query(`CREATE INDEX "user_idx_lowerusername_username" ON "user" ("username") `); + await queryRunner.query(`CREATE INDEX "IDX_7e1d93d646c13a3a0a2c7e2d5a" ON "feed" ("userId") `); + await queryRunner.query(`CREATE INDEX "idx_user_transaction_status_updated_at" ON "user_transaction" ("status", "updatedAt") `); + await queryRunner.query(`CREATE INDEX "idx_user_transaction_createdAt_desc" ON "user_transaction" ("createdAt") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_post_canonicalUrl" ON "post" ("canonicalUrl") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_post_url" ON "post" ("url") `); + await queryRunner.query(`CREATE INDEX "IDX_e74e00c9de47272d1a9ea327ab" ON "view" ("postId", "userId") `); + await queryRunner.query(`CREATE INDEX "IDX_9cb4aa5be0760354054764eefb" ON "view" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_339031b134e88d3096bfaf928c" ON "view" ("timestamp") `); + await queryRunner.query(`CREATE INDEX "IDX_82db04e5b5686aec67abf4577e" ON "view" ("postId") `); + await queryRunner.query(`ALTER TABLE "question" ADD CONSTRAINT "FK_question_screening_opportunity_id" FOREIGN KEY ("opportunityId") REFERENCES "opportunity"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_organization_dataset_location_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_company" ADD CONSTRAINT "FK_9c279d6cf291c858efa8a6b143f" FOREIGN KEY ("companyId") REFERENCES "company"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "source_feed" ADD CONSTRAINT "FK_4f708e773c1df9c288180fbb555" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "source_display" ADD CONSTRAINT "FK_ad5e759962cd86ff322cb480d09" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "settings" ADD CONSTRAINT "FK_9175e059b0a720536f7726a88c7" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_tag" ADD CONSTRAINT "FK_f3c0b831daae119196482c99379" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_report" ADD CONSTRAINT "FK_d1d5a13218f895570f4d7ad5897" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_tag" ADD CONSTRAINT "FK_8c6d05462bc68459e00f165d51c" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_a4ead2fdceb9998b4dc4d01935b" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_b08384d9c394e68429a9eea4df7" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_c46745c935040ef6188c9cbf013" FOREIGN KEY ("advancedSettingsId") REFERENCES "advanced_settings"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_01d5bc49fc5802c4e067d7ba4c9" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "dev_card" ADD CONSTRAINT "FK_70a77f197a0f92324256c983fc6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_b0df46b7939819e8bc14cf9f45c" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "alerts" ADD CONSTRAINT "FK_f2678f7b11e5128abbbc4511906" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_user_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_personalized_digest" ADD CONSTRAINT "FK_user_personalized_digest_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_transaction" ADD CONSTRAINT "FK_1422cc85c1642eed91e947cf877" FOREIGN KEY ("receiverId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_cb2ce95b74f2ee583447362d508" FOREIGN KEY ("parentId") REFERENCES "comment"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_f1305337f54e8211d5a84da0cc5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_d82dbf00f15d651edf917c8167f" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_a6977adf724e068f6faae2914da" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_231b13306df1d5046ffa7c4568b" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post_keyword" ADD CONSTRAINT "FK_88d97436b07e1462d5a7877dcb3" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_a51326b93176f2b2ebf3eda9fef" FOREIGN KEY ("scoutId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_f1bb0e9a2279673a76520d2adc5" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_83c8ae07417cd6de65b8b994587" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "view" ADD CONSTRAINT "FK_82db04e5b5686aec67abf4577e9" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`CREATE MATERIALIZED VIEW "source_tag_view" AS SELECT "s"."id" as "sourceId", "pk"."keyword" AS tag, count("pk"."keyword") AS count FROM "public"."source" "s" INNER JOIN "public"."post" "p" ON "p"."sourceId" = "s"."id" AND "p"."createdAt" > (current_timestamp - interval '90 day')::date INNER JOIN "public"."post_keyword" "pk" ON "pk"."postId" = "p"."id" AND "pk"."status" = 'allow' WHERE ("s"."active" = true AND "s"."private" = false) GROUP BY "s"."id", tag`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","source_tag_view","SELECT \"s\".\"id\" as \"sourceId\", \"pk\".\"keyword\" AS tag, count(\"pk\".\"keyword\") AS count FROM \"public\".\"source\" \"s\" INNER JOIN \"public\".\"post\" \"p\" ON \"p\".\"sourceId\" = \"s\".\"id\" AND \"p\".\"createdAt\" > (current_timestamp - interval '90 day')::date INNER JOIN \"public\".\"post_keyword\" \"pk\" ON \"pk\".\"postId\" = \"p\".\"id\" AND \"pk\".\"status\" = 'allow' WHERE (\"s\".\"active\" = true AND \"s\".\"private\" = false) GROUP BY \"s\".\"id\", tag"]); + await queryRunner.query(`CREATE MATERIALIZED VIEW "popular_video_source" AS SELECT "sourceId", avg(r) r, count(*) posts FROM "public"."popular_video_source" "base" GROUP BY "sourceId" HAVING count(*) > 5 ORDER BY r DESC`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","popular_video_source","SELECT \"sourceId\", avg(r) r, count(*) posts FROM \"public\".\"popular_video_source\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 5 ORDER BY r DESC"]); + await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0) + FROM "user" + WHERE "referralId" = u."id" + ) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0) + FROM "post" p + WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") + AND p."visible" = TRUE + AND p."deleted" = FALSE + ) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0) + FROM "post" p + WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") + AND p."visible" = TRUE + AND p."deleted" = FALSE + ) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0) + FROM "comment" c + WHERE c."userId" = u."id" + ) AS "commentUpvotes", (SELECT COALESCE(COUNT(*), 0) + FROM "user_top_reader" utp + WHERE utp."userId" = u."id" + ) AS "topReaderBadges" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != '404'`); + await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user_top_reader\" utp\n WHERE utp.\"userId\" = u.\"id\"\n ) AS \"topReaderBadges\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != '404'"]); + } + +} From 1cc9cb4dc531bbf369b3fcac12f2606ec592c4ec Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 11:03:35 +0200 Subject: [PATCH 3/7] fix: migration --- .../1769156534090-AddUserHotTakeUpvotes.ts | 538 ++---------------- 1 file changed, 57 insertions(+), 481 deletions(-) diff --git a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts index 72939ac97d..2e13dbed13 100644 --- a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts +++ b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts @@ -1,486 +1,62 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class AddUserHotTakeUpvotes1769156534090 implements MigrationInterface { - name = 'AddUserHotTakeUpvotes1769156534090' + name = 'AddUserHotTakeUpvotes1769156534090'; - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats"`); - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","popular_video_source","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "popular_video_source"`); - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","source_tag_view","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "source_tag_view"`); - await queryRunner.query(`ALTER TABLE "view" DROP CONSTRAINT "FK_82db04e5b5686aec67abf4577e9"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_83c8ae07417cd6de65b8b994587"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_f1bb0e9a2279673a76520d2adc5"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_a51326b93176f2b2ebf3eda9fef"`); - await queryRunner.query(`ALTER TABLE "post_keyword" DROP CONSTRAINT "FK_88d97436b07e1462d5a7877dcb3"`); - await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_231b13306df1d5046ffa7c4568b"`); - await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_a6977adf724e068f6faae2914da"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_d82dbf00f15d651edf917c8167f"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_f1305337f54e8211d5a84da0cc5"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_cb2ce95b74f2ee583447362d508"`); - await queryRunner.query(`ALTER TABLE "user_transaction" DROP CONSTRAINT "FK_1422cc85c1642eed91e947cf877"`); - await queryRunner.query(`ALTER TABLE "user_personalized_digest" DROP CONSTRAINT "FK_user_personalized_digest_user"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_user_locationId"`); - await queryRunner.query(`ALTER TABLE "alerts" DROP CONSTRAINT "FK_f2678f7b11e5128abbbc4511906"`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_b0df46b7939819e8bc14cf9f45c"`); - await queryRunner.query(`ALTER TABLE "dev_card" DROP CONSTRAINT "FK_70a77f197a0f92324256c983fc6"`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_01d5bc49fc5802c4e067d7ba4c9"`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_c46745c935040ef6188c9cbf013"`); - await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_b08384d9c394e68429a9eea4df7"`); - await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_a4ead2fdceb9998b4dc4d01935b"`); - await queryRunner.query(`ALTER TABLE "feed_tag" DROP CONSTRAINT "FK_8c6d05462bc68459e00f165d51c"`); - await queryRunner.query(`ALTER TABLE "post_report" DROP CONSTRAINT "FK_d1d5a13218f895570f4d7ad5897"`); - await queryRunner.query(`ALTER TABLE "post_tag" DROP CONSTRAINT "FK_f3c0b831daae119196482c99379"`); - await queryRunner.query(`ALTER TABLE "settings" DROP CONSTRAINT "FK_9175e059b0a720536f7726a88c7"`); - await queryRunner.query(`ALTER TABLE "source_display" DROP CONSTRAINT "FK_ad5e759962cd86ff322cb480d09"`); - await queryRunner.query(`ALTER TABLE "source_feed" DROP CONSTRAINT "FK_4f708e773c1df9c288180fbb555"`); - await queryRunner.query(`ALTER TABLE "user_company" DROP CONSTRAINT "FK_9c279d6cf291c858efa8a6b143f"`); - await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_organization_dataset_location_locationId"`); - await queryRunner.query(`ALTER TABLE "question" DROP CONSTRAINT "FK_question_screening_opportunity_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_82db04e5b5686aec67abf4577e"`); - await queryRunner.query(`DROP INDEX "public"."IDX_339031b134e88d3096bfaf928c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_9cb4aa5be0760354054764eefb"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e74e00c9de47272d1a9ea327ab"`); - await queryRunner.query(`DROP INDEX "public"."IDX_post_url"`); - await queryRunner.query(`DROP INDEX "public"."IDX_post_canonicalUrl"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_transaction_createdAt_desc"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_transaction_status_updated_at"`); - await queryRunner.query(`DROP INDEX "public"."IDX_7e1d93d646c13a3a0a2c7e2d5a"`); - await queryRunner.query(`DROP INDEX "public"."user_idx_lowerusername_username"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_gin_username"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_gin_name"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_reputation"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_cioRegistered"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b2e3f7568dafa9e86ae0391011"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_streak_currentstreak_userid"`); - await queryRunner.query(`DROP INDEX "public"."idx_user_streak_totalstreak_userid"`); - await queryRunner.query(`DROP INDEX "public"."IDX_85dda8e9982027f27696273a20"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b0df46b7939819e8bc14cf9f45"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a56238722b511b0be1ce2ef260"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b08384d9c394e68429a9eea4df"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a4ead2fdceb9998b4dc4d01935"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f3c0b831daae119196482c9937"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cc6fec511089f9ea5017b15d77"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c41a183d0b219907f07efa3e11"`); - await queryRunner.query(`DROP INDEX "public"."IDX_9caaea7864887299e2cb4ef355"`); - await queryRunner.query(`ALTER TABLE "dataset_location" DROP CONSTRAINT "CHK_dataset_location_country_or_continent"`); - await queryRunner.query(`CREATE TABLE "user_hot_take_upvote" ("hotTakeId" uuid NOT NULL, "userId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_0fb6851f42e1d3f4138e3e4b8ca" PRIMARY KEY ("hotTakeId", "userId"))`); - await queryRunner.query(`CREATE INDEX "IDX_902a36de7917e47ab8839ae46f" ON "user_hot_take_upvote" ("hotTakeId", "createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_cc451486cbf598411e27264102" ON "user_hot_take_upvote" ("userId", "createdAt") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0fb6851f42e1d3f4138e3e4b8c" ON "user_hot_take_upvote" ("hotTakeId", "userId") `); - await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "upvotes" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "user_integration" ALTER COLUMN "meta" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_source_integration" ALTER COLUMN "channelIds" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberPostingRank" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberInviteRank" DROP NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_status_value"`); - await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_value_occurrences"`); - await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_occ_value"`); - await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status"`); - await queryRunner.query(`ALTER TABLE "keyword" DROP COLUMN "status"`); - await queryRunner.query(`ALTER TABLE "keyword" ADD "status" text NOT NULL DEFAULT 'pending'`); - await queryRunner.query(`DROP INDEX "public"."IDX_content_preference_reference_user_id"`); - await queryRunner.query(`ALTER TABLE "content_preference" DROP COLUMN "referenceUserId"`); - await queryRunner.query(`ALTER TABLE "content_preference" ADD "referenceUserId" character varying`); - await queryRunner.query(`ALTER TABLE "comment" ALTER COLUMN "contentHtml" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "feed" DROP CONSTRAINT "FK_70952a3f1b3717e7021a439edda"`); - await queryRunner.query(`DROP INDEX "public"."IDX_feed_id_user_id"`); - await queryRunner.query(`ALTER TABLE "feed" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "feed" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_hot_take_user_id"`); - await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_ff0f49b797aca629f81cef47610"`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_ff0f49b797aca629f81cef47610" UNIQUE ("defaultFeedId")`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "FK_c025478b45e60017ed10c77f99c"`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type")`); - await queryRunner.query(`ALTER TABLE "user_action" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type", "userId")`); - await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`); - await queryRunner.query(`ALTER TABLE "user_stack" DROP CONSTRAINT "FK_user_stack_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_stack_user_id"`); - await queryRunner.query(`ALTER TABLE "user_stack" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_stack" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP CONSTRAINT "FK_user_workspace_photo_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_workspace_photo_user_id"`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_gear" DROP CONSTRAINT "FK_user_gear_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_gear_user_id"`); - await queryRunner.query(`ALTER TABLE "user_gear" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_gear" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_tool" DROP CONSTRAINT "FK_user_tool_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_tool_user_id"`); - await queryRunner.query(`ALTER TABLE "user_tool" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_tool" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bookmark_userId_createdAt"`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId")`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId", "userId")`); - await queryRunner.query(`ALTER TABLE "bookmark_list" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "bookmark_list" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "submission" ALTER COLUMN "reason" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "FK_c8721bd56ae600308745ad49744"`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType")`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD "userId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType", "userId")`); - await queryRunner.query(`ALTER TABLE "user_candidate_preference" ALTER COLUMN "locationType" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "dataset_location" ADD CONSTRAINT "UQ_3d2d01d9c20653dd58e6741666c" UNIQUE ("externalId")`); - await queryRunner.query(`CREATE INDEX "IDX_19da087dd68a0bc5e5302ca9a5" ON "view" ("postId") `); - await queryRunner.query(`CREATE INDEX "IDX_c6af9853ff6a60d2e80dd8b3af" ON "view" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_10dff5dbc360f0d47cada787c7" ON "view" ("timestamp") `); - await queryRunner.query(`CREATE INDEX "IDX_c63ec25b983985f2fee951afcc" ON "view" ("postId", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_post_awards" ON "post" ("awards") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2d4cb7f2ff3bcc12f0639d8f86" ON "post" ("url") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_02634e624fee03af415a7597ed" ON "post" ("canonicalUrl") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2d4cb7f2ff3bcc12f0639d8f86" ON "post" ("url") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_02634e624fee03af415a7597ed" ON "post" ("canonicalUrl") `); - await queryRunner.query(`CREATE INDEX "IDX_b499447822de3f24ad355e19b8" ON "post" ("type") `); - await queryRunner.query(`CREATE INDEX "IDX_keyword_status" ON "keyword" ("status") `); - await queryRunner.query(`CREATE INDEX "IDX_keyword_status_value_occurrences" ON "keyword" ("status", "value", "occurrences") `); - await queryRunner.query(`CREATE INDEX "IDX_status_value" ON "keyword" ("status", "value") `); - await queryRunner.query(`CREATE INDEX "IDX_content_preference_reference_user_id" ON "content_preference" ("referenceUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_comment_awards" ON "comment" ("awards") `); - await queryRunner.query(`CREATE INDEX "IDX_70952a3f1b3717e7021a439edd" ON "feed" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_feed_id_user_id" ON "feed" ("id", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_user_id" ON "user_hot_take" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_c025478b45e60017ed10c77f99" ON "user_action" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_stack_user_id" ON "user_stack" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_workspace_photo_user_id" ON "user_workspace_photo" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_gear_user_id" ON "user_gear" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_tool_user_id" ON "user_tool" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_f2678f7b11e5128abbbc451190" ON "alerts" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_ee4a9b84b65399f9a6581d9a9a" ON "bookmark" ("postId") `); - await queryRunner.query(`CREATE INDEX "IDX_ff105847cfef10dd4af15b52a9" ON "bookmark_list" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_a9b3d81d71cd6c2cd82249f689" ON "feed_source" ("feedId") `); - await queryRunner.query(`CREATE INDEX "IDX_79143dac6de88d4ba0f0ecfa0d" ON "feed_source" ("sourceId") `); - await queryRunner.query(`CREATE INDEX "IDX_444c1b4f6cd7b632277f557935" ON "post_tag" ("postId") `); - await queryRunner.query(`CREATE INDEX "IDX_a72fa6b0a0b9ea438fbef00cb3" ON "source_display" ("userId", "enabled") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_bf5818e5de16bc943005a50166" ON "source_display" ("sourceId", "userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f83844a38b17aa07772469cb08" ON "source_feed" ("sourceId", "feed") `); - await queryRunner.query(`CREATE INDEX "IDX_6d32bd7a8976dac90c4e11b340" ON "tag_segment" ("segment") `); - await queryRunner.query(`CREATE INDEX "IDX_65a9ca0600dbc72c6ff76501a6" ON "notification_preference" ("type") `); - await queryRunner.query(`ALTER TABLE "view" ADD CONSTRAINT "FK_19da087dd68a0bc5e5302ca9a59" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_0b78981ffc8817ce54da9180a8d" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_1609023c77409ed0c4388ec240e" FOREIGN KEY ("scoutId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_keyword" ADD CONSTRAINT "FK_b96fa78416c5366186a32b9dd45" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_cea0cfe859c4cb4e35da879e111" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_94a85bb16d24033a2afdd5df060" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_c0354a9a009d3bb45a08655ce3b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_e3aebe2bd1c53467a07109be596" FOREIGN KEY ("parentId") REFERENCES "comment"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_transaction" ADD CONSTRAINT "FK_bb9c8cc52dbf33107f613fb8302" FOREIGN KEY ("receiverId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed" ADD CONSTRAINT "FK_70952a3f1b3717e7021a439edda" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_hot_take" ADD CONSTRAINT "FK_user_hot_take_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" ADD CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" FOREIGN KEY ("hotTakeId") REFERENCES "user_hot_take"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" ADD CONSTRAINT "FK_user_hot_take_upvote_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_ff0f49b797aca629f81cef47610" FOREIGN KEY ("defaultFeedId") REFERENCES "feed"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_user_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "FK_c025478b45e60017ed10c77f99c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_stack" ADD CONSTRAINT "FK_user_stack_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD CONSTRAINT "FK_user_workspace_photo_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_gear" ADD CONSTRAINT "FK_user_gear_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_tool" ADD CONSTRAINT "FK_user_tool_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_ee4a9b84b65399f9a6581d9a9a5" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "bookmark_list" ADD CONSTRAINT "FK_ff105847cfef10dd4af15b52a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "dev_card" ADD CONSTRAINT "FK_338150b6c9b6d9b788d285d1c95" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_eaa6a8fb25235695fb7f99af859" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_67f0b1889dd335ea2c196146f82" FOREIGN KEY ("advancedSettingsId") REFERENCES "advanced_settings"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_a9b3d81d71cd6c2cd82249f6895" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_79143dac6de88d4ba0f0ecfa0d9" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_tag" ADD CONSTRAINT "FK_e875186f4a0f01b72cc3e79eaee" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_report" ADD CONSTRAINT "FK_44b0e753044952524eb47ac6f40" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_tag" ADD CONSTRAINT "FK_444c1b4f6cd7b632277f5579354" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "source_display" ADD CONSTRAINT "FK_b65c592c197135747cab64e986a" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "source_feed" ADD CONSTRAINT "FK_725e606248d79f0f10d0fe730c0" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "FK_c8721bd56ae600308745ad49744" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_organization_dataset_location_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "question" ADD CONSTRAINT "FK_question_feedback_opportunity_id" FOREIGN KEY ("opportunityId") REFERENCES "opportunity"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_analytics_history" ADD CONSTRAINT "FK_6bb82232882b9a8bfa54a034c5f" FOREIGN KEY ("id") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_analytics" ADD CONSTRAINT "FK_421771f55c623cd4f6103828196" FOREIGN KEY ("id") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`CREATE MATERIALIZED VIEW "source_tag_view" AS SELECT "s"."id" as sourceId, "pk"."keyword" AS tag, count("pk"."keyword") AS count FROM "public"."source" "s" INNER JOIN "public"."post" "p" ON "p"."sourceId" = "s"."id" AND "p"."createdAt" > :time INNER JOIN "public"."post_keyword" "pk" ON "pk"."postId" = "p"."id" AND "pk"."status" = :status WHERE ("s"."active" = :orm_param_0 AND "s"."private" = :orm_param_1) GROUP BY sourceId, tag`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","source_tag_view","SELECT \"s\".\"id\" as sourceId, \"pk\".\"keyword\" AS tag, count(\"pk\".\"keyword\") AS count FROM \"public\".\"source\" \"s\" INNER JOIN \"public\".\"post\" \"p\" ON \"p\".\"sourceId\" = \"s\".\"id\" AND \"p\".\"createdAt\" > :time INNER JOIN \"public\".\"post_keyword\" \"pk\" ON \"pk\".\"postId\" = \"p\".\"id\" AND \"pk\".\"status\" = :status WHERE (\"s\".\"active\" = :orm_param_0 AND \"s\".\"private\" = :orm_param_1) GROUP BY sourceId, tag"]); - await queryRunner.query(`CREATE MATERIALIZED VIEW "popular_video_source" AS SELECT "sourceId", avg(r) r, count(*) posts FROM "public"."popular_video_post" "base" GROUP BY "sourceId" HAVING count(*) > 5 ORDER BY r DESC`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","popular_video_source","SELECT \"sourceId\", avg(r) r, count(*) posts FROM \"public\".\"popular_video_post\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 5 ORDER BY r DESC"]); - await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0) - FROM "user" - WHERE "referralId" = u."id" - ) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0) - FROM "post" p - WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") - AND p."visible" = TRUE - AND p."deleted" = FALSE - ) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0) - FROM "post" p - WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") - AND p."visible" = TRUE - AND p."deleted" = FALSE - ) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0) - FROM "comment" c - WHERE c."userId" = u."id" - ) AS "commentUpvotes", (SELECT COALESCE(COUNT(*), 0) - FROM "user_top_reader" utp - WHERE utp."userId" = u."id" - ) AS "topReaderBadges" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != :ghostId`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user_top_reader\" utp\n WHERE utp.\"userId\" = u.\"id\"\n ) AS \"topReaderBadges\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != :ghostId"]); - await queryRunner.query(`CREATE INDEX "IDX_sourceTag_sourceId" ON "source_tag_view" ("sourceId") `); - await queryRunner.query(`CREATE INDEX "IDX_sourceTag_tag" ON "source_tag_view" ("tag") `); - await queryRunner.query(`CREATE INDEX "IDX_user_stats_id" ON "user_stats" ("id") `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_user_stats_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_sourceTag_tag"`); - await queryRunner.query(`DROP INDEX "public"."IDX_sourceTag_sourceId"`); - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","user_stats","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "user_stats"`); - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","popular_video_source","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "popular_video_source"`); - await queryRunner.query(`DELETE FROM "public"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, ["MATERIALIZED_VIEW","source_tag_view","public"]); - await queryRunner.query(`DROP MATERIALIZED VIEW "source_tag_view"`); - await queryRunner.query(`ALTER TABLE "post_analytics" DROP CONSTRAINT "FK_421771f55c623cd4f6103828196"`); - await queryRunner.query(`ALTER TABLE "post_analytics_history" DROP CONSTRAINT "FK_6bb82232882b9a8bfa54a034c5f"`); - await queryRunner.query(`ALTER TABLE "question" DROP CONSTRAINT "FK_question_feedback_opportunity_id"`); - await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_organization_dataset_location_locationId"`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "FK_c8721bd56ae600308745ad49744"`); - await queryRunner.query(`ALTER TABLE "source_feed" DROP CONSTRAINT "FK_725e606248d79f0f10d0fe730c0"`); - await queryRunner.query(`ALTER TABLE "source_display" DROP CONSTRAINT "FK_b65c592c197135747cab64e986a"`); - await queryRunner.query(`ALTER TABLE "post_tag" DROP CONSTRAINT "FK_444c1b4f6cd7b632277f5579354"`); - await queryRunner.query(`ALTER TABLE "post_report" DROP CONSTRAINT "FK_44b0e753044952524eb47ac6f40"`); - await queryRunner.query(`ALTER TABLE "feed_tag" DROP CONSTRAINT "FK_e875186f4a0f01b72cc3e79eaee"`); - await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_79143dac6de88d4ba0f0ecfa0d9"`); - await queryRunner.query(`ALTER TABLE "feed_source" DROP CONSTRAINT "FK_a9b3d81d71cd6c2cd82249f6895"`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_67f0b1889dd335ea2c196146f82"`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" DROP CONSTRAINT "FK_eaa6a8fb25235695fb7f99af859"`); - await queryRunner.query(`ALTER TABLE "dev_card" DROP CONSTRAINT "FK_338150b6c9b6d9b788d285d1c95"`); - await queryRunner.query(`ALTER TABLE "bookmark_list" DROP CONSTRAINT "FK_ff105847cfef10dd4af15b52a9a"`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b"`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "FK_ee4a9b84b65399f9a6581d9a9a5"`); - await queryRunner.query(`ALTER TABLE "user_tool" DROP CONSTRAINT "FK_user_tool_user_id"`); - await queryRunner.query(`ALTER TABLE "user_gear" DROP CONSTRAINT "FK_user_gear_user_id"`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP CONSTRAINT "FK_user_workspace_photo_user_id"`); - await queryRunner.query(`ALTER TABLE "user_stack" DROP CONSTRAINT "FK_user_stack_user_id"`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "FK_c025478b45e60017ed10c77f99c"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_user_locationId"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_ff0f49b797aca629f81cef47610"`); - await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_user_id"`); - await queryRunner.query(`ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_hot_take_id"`); - await queryRunner.query(`ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_user_id"`); - await queryRunner.query(`ALTER TABLE "feed" DROP CONSTRAINT "FK_70952a3f1b3717e7021a439edda"`); - await queryRunner.query(`ALTER TABLE "user_transaction" DROP CONSTRAINT "FK_bb9c8cc52dbf33107f613fb8302"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_e3aebe2bd1c53467a07109be596"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_c0354a9a009d3bb45a08655ce3b"`); - await queryRunner.query(`ALTER TABLE "comment" DROP CONSTRAINT "FK_94a85bb16d24033a2afdd5df060"`); - await queryRunner.query(`ALTER TABLE "content_preference" DROP CONSTRAINT "FK_cea0cfe859c4cb4e35da879e111"`); - await queryRunner.query(`ALTER TABLE "post_keyword" DROP CONSTRAINT "FK_b96fa78416c5366186a32b9dd45"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_c6fb082a3114f35d0cc27c518e0"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_1609023c77409ed0c4388ec240e"`); - await queryRunner.query(`ALTER TABLE "post" DROP CONSTRAINT "FK_0b78981ffc8817ce54da9180a8d"`); - await queryRunner.query(`ALTER TABLE "view" DROP CONSTRAINT "FK_19da087dd68a0bc5e5302ca9a59"`); - await queryRunner.query(`DROP INDEX "public"."IDX_65a9ca0600dbc72c6ff76501a6"`); - await queryRunner.query(`DROP INDEX "public"."IDX_6d32bd7a8976dac90c4e11b340"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f83844a38b17aa07772469cb08"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bf5818e5de16bc943005a50166"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a72fa6b0a0b9ea438fbef00cb3"`); - await queryRunner.query(`DROP INDEX "public"."IDX_444c1b4f6cd7b632277f557935"`); - await queryRunner.query(`DROP INDEX "public"."IDX_79143dac6de88d4ba0f0ecfa0d"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a9b3d81d71cd6c2cd82249f689"`); - await queryRunner.query(`DROP INDEX "public"."IDX_ff105847cfef10dd4af15b52a9"`); - await queryRunner.query(`DROP INDEX "public"."IDX_ee4a9b84b65399f9a6581d9a9a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f2678f7b11e5128abbbc451190"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_tool_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_gear_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_workspace_photo_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_stack_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c025478b45e60017ed10c77f99"`); - await queryRunner.query(`DROP INDEX "public"."IDX_user_hot_take_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_feed_id_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_70952a3f1b3717e7021a439edd"`); - await queryRunner.query(`DROP INDEX "public"."IDX_comment_awards"`); - await queryRunner.query(`DROP INDEX "public"."IDX_content_preference_reference_user_id"`); - await queryRunner.query(`DROP INDEX "public"."IDX_status_value"`); - await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status_value_occurrences"`); - await queryRunner.query(`DROP INDEX "public"."IDX_keyword_status"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b499447822de3f24ad355e19b8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_02634e624fee03af415a7597ed"`); - await queryRunner.query(`DROP INDEX "public"."IDX_2d4cb7f2ff3bcc12f0639d8f86"`); - await queryRunner.query(`DROP INDEX "public"."IDX_02634e624fee03af415a7597ed"`); - await queryRunner.query(`DROP INDEX "public"."IDX_2d4cb7f2ff3bcc12f0639d8f86"`); - await queryRunner.query(`DROP INDEX "public"."IDX_post_awards"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c63ec25b983985f2fee951afcc"`); - await queryRunner.query(`DROP INDEX "public"."IDX_10dff5dbc360f0d47cada787c7"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c6af9853ff6a60d2e80dd8b3af"`); - await queryRunner.query(`DROP INDEX "public"."IDX_19da087dd68a0bc5e5302ca9a5"`); - await queryRunner.query(`ALTER TABLE "dataset_location" DROP CONSTRAINT "UQ_3d2d01d9c20653dd58e6741666c"`); - await queryRunner.query(`ALTER TABLE "user_candidate_preference" ALTER COLUMN "locationType" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "notificationType")`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD "userId" text NOT NULL`); - await queryRunner.query(`ALTER TABLE "notification_preference" DROP CONSTRAINT "PK_de66bee12eefee879479c27f94f"`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "PK_de66bee12eefee879479c27f94f" PRIMARY KEY ("referenceId", "userId", "notificationType")`); - await queryRunner.query(`ALTER TABLE "notification_preference" ADD CONSTRAINT "FK_c8721bd56ae600308745ad49744" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "submission" ALTER COLUMN "reason" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "bookmark_list" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "bookmark_list" ADD "userId" text NOT NULL`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId")`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD "userId" text NOT NULL`); - await queryRunner.query(`ALTER TABLE "bookmark" DROP CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac"`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "PK_ffde759b87b2d3af9a6fae265ac" PRIMARY KEY ("postId", "userId")`); - await queryRunner.query(`CREATE INDEX "IDX_bookmark_userId_createdAt" ON "bookmark" ("createdAt", "userId") `); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_e389fc192c59bdce0847ef9ef8b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_tool" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_tool" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_user_tool_user_id" ON "user_tool" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_tool" ADD CONSTRAINT "FK_user_tool_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_gear" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_gear" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_user_gear_user_id" ON "user_gear" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_gear" ADD CONSTRAINT "FK_user_gear_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_user_workspace_photo_user_id" ON "user_workspace_photo" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_workspace_photo" ADD CONSTRAINT "FK_user_workspace_photo_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_stack" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_stack" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_user_stack_user_id" ON "user_stack" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_stack" ADD CONSTRAINT "FK_user_stack_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "user_top_reader" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("type")`); - await queryRunner.query(`ALTER TABLE "user_action" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD "userId" text NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_action" DROP CONSTRAINT "PK_d2be31969535c36966ac5b76410"`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "PK_d2be31969535c36966ac5b76410" PRIMARY KEY ("userId", "type")`); - await queryRunner.query(`ALTER TABLE "user_action" ADD CONSTRAINT "FK_c025478b45e60017ed10c77f99c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_ff0f49b797aca629f81cef47610"`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_ff0f49b797aca629f81cef47610" FOREIGN KEY ("defaultFeedId") REFERENCES "feed"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "user_hot_take" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_user_id" ON "user_hot_take" ("userId") `); - await queryRunner.query(`ALTER TABLE "user_hot_take" ADD CONSTRAINT "FK_user_hot_take_user_id" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed" DROP COLUMN "userId"`); - await queryRunner.query(`ALTER TABLE "feed" ADD "userId" text NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_feed_id_user_id" ON "feed" ("id", "userId") `); - await queryRunner.query(`ALTER TABLE "feed" ADD CONSTRAINT "FK_70952a3f1b3717e7021a439edda" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ALTER COLUMN "contentHtml" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "content_preference" DROP COLUMN "referenceUserId"`); - await queryRunner.query(`ALTER TABLE "content_preference" ADD "referenceUserId" text`); - await queryRunner.query(`CREATE INDEX "IDX_content_preference_reference_user_id" ON "content_preference" ("referenceUserId") `); - await queryRunner.query(`ALTER TABLE "keyword" DROP COLUMN "status"`); - await queryRunner.query(`ALTER TABLE "keyword" ADD "status" character varying NOT NULL DEFAULT 'pending'`); - await queryRunner.query(`CREATE INDEX "IDX_keyword_status" ON "keyword" ("status") `); - await queryRunner.query(`CREATE INDEX "IDX_keyword_status_occ_value" ON "keyword" ("occurrences", "status", "value") `); - await queryRunner.query(`CREATE INDEX "IDX_keyword_status_value_occurrences" ON "keyword" ("occurrences", "status", "value") `); - await queryRunner.query(`CREATE INDEX "IDX_status_value" ON "keyword" ("status", "value") `); - await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberInviteRank" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "source" ALTER COLUMN "memberPostingRank" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_source_integration" ALTER COLUMN "channelIds" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_integration" ALTER COLUMN "meta" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "user_hot_take" DROP COLUMN "upvotes"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0fb6851f42e1d3f4138e3e4b8c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cc451486cbf598411e27264102"`); - await queryRunner.query(`DROP INDEX "public"."IDX_902a36de7917e47ab8839ae46f"`); - await queryRunner.query(`DROP TABLE "user_hot_take_upvote"`); - await queryRunner.query(`ALTER TABLE "dataset_location" ADD CONSTRAINT "CHK_dataset_location_country_or_continent" CHECK (((country IS NOT NULL) OR (continent IS NOT NULL)))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9caaea7864887299e2cb4ef355" ON "source_feed" ("feed", "sourceId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c41a183d0b219907f07efa3e11" ON "source_display" ("sourceId", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_cc6fec511089f9ea5017b15d77" ON "source_display" ("enabled", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_f3c0b831daae119196482c9937" ON "post_tag" ("postId") `); - await queryRunner.query(`CREATE INDEX "IDX_a4ead2fdceb9998b4dc4d01935" ON "feed_source" ("sourceId") `); - await queryRunner.query(`CREATE INDEX "IDX_b08384d9c394e68429a9eea4df" ON "feed_source" ("feedId") `); - await queryRunner.query(`CREATE INDEX "IDX_a56238722b511b0be1ce2ef260" ON "bookmark" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_b0df46b7939819e8bc14cf9f45" ON "bookmark" ("postId") `); - await queryRunner.query(`CREATE INDEX "IDX_85dda8e9982027f27696273a20" ON "alerts" ("userId") `); - await queryRunner.query(`CREATE INDEX "idx_user_streak_totalstreak_userid" ON "user_streak" ("totalStreak", "userId") `); - await queryRunner.query(`CREATE INDEX "idx_user_streak_currentstreak_userid" ON "user_streak" ("currentStreak", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_b2e3f7568dafa9e86ae0391011" ON "user_action" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_user_cioRegistered" ON "user" ("cioRegistered") `); - await queryRunner.query(`CREATE INDEX "idx_user_reputation" ON "user" ("reputation") `); - await queryRunner.query(`CREATE INDEX "idx_user_gin_name" ON "user" ("name") `); - await queryRunner.query(`CREATE INDEX "idx_user_gin_username" ON "user" ("username") `); - await queryRunner.query(`CREATE INDEX "user_idx_lowerusername_username" ON "user" ("username") `); - await queryRunner.query(`CREATE INDEX "IDX_7e1d93d646c13a3a0a2c7e2d5a" ON "feed" ("userId") `); - await queryRunner.query(`CREATE INDEX "idx_user_transaction_status_updated_at" ON "user_transaction" ("status", "updatedAt") `); - await queryRunner.query(`CREATE INDEX "idx_user_transaction_createdAt_desc" ON "user_transaction" ("createdAt") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_post_canonicalUrl" ON "post" ("canonicalUrl") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_post_url" ON "post" ("url") `); - await queryRunner.query(`CREATE INDEX "IDX_e74e00c9de47272d1a9ea327ab" ON "view" ("postId", "userId") `); - await queryRunner.query(`CREATE INDEX "IDX_9cb4aa5be0760354054764eefb" ON "view" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_339031b134e88d3096bfaf928c" ON "view" ("timestamp") `); - await queryRunner.query(`CREATE INDEX "IDX_82db04e5b5686aec67abf4577e" ON "view" ("postId") `); - await queryRunner.query(`ALTER TABLE "question" ADD CONSTRAINT "FK_question_screening_opportunity_id" FOREIGN KEY ("opportunityId") REFERENCES "opportunity"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_organization_dataset_location_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_company" ADD CONSTRAINT "FK_9c279d6cf291c858efa8a6b143f" FOREIGN KEY ("companyId") REFERENCES "company"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "source_feed" ADD CONSTRAINT "FK_4f708e773c1df9c288180fbb555" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "source_display" ADD CONSTRAINT "FK_ad5e759962cd86ff322cb480d09" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "settings" ADD CONSTRAINT "FK_9175e059b0a720536f7726a88c7" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_tag" ADD CONSTRAINT "FK_f3c0b831daae119196482c99379" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_report" ADD CONSTRAINT "FK_d1d5a13218f895570f4d7ad5897" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_tag" ADD CONSTRAINT "FK_8c6d05462bc68459e00f165d51c" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_a4ead2fdceb9998b4dc4d01935b" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_source" ADD CONSTRAINT "FK_b08384d9c394e68429a9eea4df7" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_c46745c935040ef6188c9cbf013" FOREIGN KEY ("advancedSettingsId") REFERENCES "advanced_settings"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "feed_advanced_settings" ADD CONSTRAINT "FK_01d5bc49fc5802c4e067d7ba4c9" FOREIGN KEY ("feedId") REFERENCES "feed"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "dev_card" ADD CONSTRAINT "FK_70a77f197a0f92324256c983fc6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "bookmark" ADD CONSTRAINT "FK_b0df46b7939819e8bc14cf9f45c" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "alerts" ADD CONSTRAINT "FK_f2678f7b11e5128abbbc4511906" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_user_locationId" FOREIGN KEY ("locationId") REFERENCES "dataset_location"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_personalized_digest" ADD CONSTRAINT "FK_user_personalized_digest_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_transaction" ADD CONSTRAINT "FK_1422cc85c1642eed91e947cf877" FOREIGN KEY ("receiverId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_cb2ce95b74f2ee583447362d508" FOREIGN KEY ("parentId") REFERENCES "comment"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_f1305337f54e8211d5a84da0cc5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "comment" ADD CONSTRAINT "FK_d82dbf00f15d651edf917c8167f" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_a6977adf724e068f6faae2914da" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "content_preference" ADD CONSTRAINT "FK_231b13306df1d5046ffa7c4568b" FOREIGN KEY ("referenceUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post_keyword" ADD CONSTRAINT "FK_88d97436b07e1462d5a7877dcb3" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_a51326b93176f2b2ebf3eda9fef" FOREIGN KEY ("scoutId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_f1bb0e9a2279673a76520d2adc5" FOREIGN KEY ("authorId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "post" ADD CONSTRAINT "FK_83c8ae07417cd6de65b8b994587" FOREIGN KEY ("sourceId") REFERENCES "source"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "view" ADD CONSTRAINT "FK_82db04e5b5686aec67abf4577e9" FOREIGN KEY ("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`CREATE MATERIALIZED VIEW "source_tag_view" AS SELECT "s"."id" as "sourceId", "pk"."keyword" AS tag, count("pk"."keyword") AS count FROM "public"."source" "s" INNER JOIN "public"."post" "p" ON "p"."sourceId" = "s"."id" AND "p"."createdAt" > (current_timestamp - interval '90 day')::date INNER JOIN "public"."post_keyword" "pk" ON "pk"."postId" = "p"."id" AND "pk"."status" = 'allow' WHERE ("s"."active" = true AND "s"."private" = false) GROUP BY "s"."id", tag`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","source_tag_view","SELECT \"s\".\"id\" as \"sourceId\", \"pk\".\"keyword\" AS tag, count(\"pk\".\"keyword\") AS count FROM \"public\".\"source\" \"s\" INNER JOIN \"public\".\"post\" \"p\" ON \"p\".\"sourceId\" = \"s\".\"id\" AND \"p\".\"createdAt\" > (current_timestamp - interval '90 day')::date INNER JOIN \"public\".\"post_keyword\" \"pk\" ON \"pk\".\"postId\" = \"p\".\"id\" AND \"pk\".\"status\" = 'allow' WHERE (\"s\".\"active\" = true AND \"s\".\"private\" = false) GROUP BY \"s\".\"id\", tag"]); - await queryRunner.query(`CREATE MATERIALIZED VIEW "popular_video_source" AS SELECT "sourceId", avg(r) r, count(*) posts FROM "public"."popular_video_source" "base" GROUP BY "sourceId" HAVING count(*) > 5 ORDER BY r DESC`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","popular_video_source","SELECT \"sourceId\", avg(r) r, count(*) posts FROM \"public\".\"popular_video_source\" \"base\" GROUP BY \"sourceId\" HAVING count(*) > 5 ORDER BY r DESC"]); - await queryRunner.query(`CREATE MATERIALIZED VIEW "user_stats" AS SELECT u."id", (SELECT COALESCE(COUNT(*), 0) - FROM "user" - WHERE "referralId" = u."id" - ) AS "referrals", (SELECT COALESCE(SUM(p."views"), 0) - FROM "post" p - WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") - AND p."visible" = TRUE - AND p."deleted" = FALSE - ) AS "views", (SELECT COALESCE(SUM(p."upvotes"), 0) - FROM "post" p - WHERE (p."authorId" = u."id" OR p."scoutId" = u."id") - AND p."visible" = TRUE - AND p."deleted" = FALSE - ) AS "postUpvotes", (SELECT COALESCE(SUM(c."upvotes"), 0) - FROM "comment" c - WHERE c."userId" = u."id" - ) AS "commentUpvotes", (SELECT COALESCE(COUNT(*), 0) - FROM "user_top_reader" utp - WHERE utp."userId" = u."id" - ) AS "topReaderBadges" FROM "public"."user" "u" WHERE "u"."infoConfirmed" = TRUE AND "u"."id" != '404'`); - await queryRunner.query(`INSERT INTO "public"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, ["public","MATERIALIZED_VIEW","user_stats","SELECT u.\"id\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user\"\n WHERE \"referralId\" = u.\"id\"\n ) AS \"referrals\", (SELECT COALESCE(SUM(p.\"views\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"views\", (SELECT COALESCE(SUM(p.\"upvotes\"), 0)\n FROM \"post\" p\n WHERE (p.\"authorId\" = u.\"id\" OR p.\"scoutId\" = u.\"id\")\n AND p.\"visible\" = TRUE\n AND p.\"deleted\" = FALSE\n ) AS \"postUpvotes\", (SELECT COALESCE(SUM(c.\"upvotes\"), 0)\n FROM \"comment\" c\n WHERE c.\"userId\" = u.\"id\"\n ) AS \"commentUpvotes\", (SELECT COALESCE(COUNT(*), 0)\n FROM \"user_top_reader\" utp\n WHERE utp.\"userId\" = u.\"id\"\n ) AS \"topReaderBadges\" FROM \"public\".\"user\" \"u\" WHERE \"u\".\"infoConfirmed\" = TRUE AND \"u\".\"id\" != '404'"]); - } + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "user_hot_take_upvote" ( + "hotTakeId" uuid NOT NULL, + "userId" character varying NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_0fb6851f42e1d3f4138e3e4b8ca" PRIMARY KEY ("hotTakeId", "userId") + ) + `); + await queryRunner.query(` + CREATE INDEX "IDX_902a36de7917e47ab8839ae46f" ON "user_hot_take_upvote" ("hotTakeId", "createdAt") + `); + await queryRunner.query(` + CREATE INDEX "IDX_cc451486cbf598411e27264102" ON "user_hot_take_upvote" ("userId", "createdAt") + `); + await queryRunner.query(` + ALTER TABLE "user_hot_take" ADD "upvotes" integer NOT NULL DEFAULT 0 + `); + await queryRunner.query(` + ALTER TABLE "user_hot_take_upvote" + ADD CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" + FOREIGN KEY ("hotTakeId") + REFERENCES "user_hot_take"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + `); + await queryRunner.query(` + ALTER TABLE "user_hot_take_upvote" + ADD CONSTRAINT "FK_user_hot_take_upvote_user_id" + FOREIGN KEY ("userId") + REFERENCES "user"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION + `); + } + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_user_id" + `); + await queryRunner.query(` + ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" + `); + await queryRunner.query(` + ALTER TABLE "user_hot_take" DROP COLUMN "upvotes" + `); + await queryRunner.query(` + DROP INDEX "public"."IDX_cc451486cbf598411e27264102" + `); + await queryRunner.query(` + DROP INDEX "public"."IDX_902a36de7917e47ab8839ae46f" + `); + await queryRunner.query(` + DROP TABLE "user_hot_take_upvote" + `); + } } From 175656b28861f1b67e09509fec4bc0ded3dc49cb Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 11:12:15 +0200 Subject: [PATCH 4/7] fix: lint --- __tests__/userHotTake.ts | 6 ++---- src/entity/user/UserHotTakeUpvote.ts | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/__tests__/userHotTake.ts b/__tests__/userHotTake.ts index 5956f0bc15..c785ae16df 100644 --- a/__tests__/userHotTake.ts +++ b/__tests__/userHotTake.ts @@ -119,7 +119,7 @@ describe('query userHotTakes with upvotes', () => { }); it('should return upvoted as null when not logged in', async () => { - const hotTake = await con.getRepository(UserHotTake).save({ + await con.getRepository(UserHotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -256,9 +256,7 @@ describe('mutation vote on hot take', () => { }, }); - expect(res.errors?.[0]?.message).toBe( - 'Hot takes do not support downvotes', - ); + expect(res.errors?.[0]?.message).toBe('Hot takes do not support downvotes'); }); it('should return error for non-existent hot take', async () => { diff --git a/src/entity/user/UserHotTakeUpvote.ts b/src/entity/user/UserHotTakeUpvote.ts index 42e52752f6..2987ab7356 100644 --- a/src/entity/user/UserHotTakeUpvote.ts +++ b/src/entity/user/UserHotTakeUpvote.ts @@ -1,5 +1,4 @@ import { - Column, CreateDateColumn, Entity, Index, From 5aacf4802df1760755ac80fc972a796de8b261f8 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 12:13:53 +0200 Subject: [PATCH 5/7] feat(profile): refactor hot take voting to follow UserPost pattern Rename UserHotTake entity to HotTake for content, create new UserHotTake join table with vote column (like UserPost). Add postgres triggers for automatic upvote count maintenance. Rename GraphQL types and resolvers from UserHotTake to HotTake for consistency. Co-Authored-By: Claude Opus 4.5 --- __tests__/userHotTake.ts | 78 +++++----- src/common/vote.ts | 36 ++--- src/entity/user/HotTake.ts | 49 ++++++ src/entity/user/UserHotTake.ts | 47 +++--- src/entity/user/UserHotTakeUpvote.ts | 45 ------ src/entity/user/index.ts | 2 +- src/graphorm/index.ts | 6 +- .../1769156534090-AddUserHotTakeUpvotes.ts | 143 ++++++++++++++---- src/schema/userHotTake.ts | 68 ++++----- 9 files changed, 281 insertions(+), 193 deletions(-) create mode 100644 src/entity/user/HotTake.ts delete mode 100644 src/entity/user/UserHotTakeUpvote.ts diff --git a/__tests__/userHotTake.ts b/__tests__/userHotTake.ts index c785ae16df..cc03dbbb64 100644 --- a/__tests__/userHotTake.ts +++ b/__tests__/userHotTake.ts @@ -10,8 +10,9 @@ import { } from './helpers'; import { User } from '../src/entity/user/User'; import { usersFixture } from './fixture/user'; +import { HotTake } from '../src/entity/user/HotTake'; import { UserHotTake } from '../src/entity/user/UserHotTake'; -import { UserHotTakeUpvote } from '../src/entity/user/UserHotTakeUpvote'; +import { UserVote } from '../src/types'; let con: DataSource; let state: GraphQLTestingState; @@ -56,7 +57,7 @@ describe('query userHotTakes', () => { }); it('should return hot takes ordered by position', async () => { - await con.getRepository(UserHotTake).save([ + await con.getRepository(HotTake).save([ { userId: '1', emoji: '🔥', title: 'Hot take 1', position: 1 }, { userId: '1', emoji: '💡', title: 'Hot take 2', position: 0 }, ]); @@ -68,7 +69,7 @@ describe('query userHotTakes', () => { }); it('should return hot takes with subtitle', async () => { - await con.getRepository(UserHotTake).save({ + await con.getRepository(HotTake).save({ userId: '1', emoji: '🎯', title: 'My opinion', @@ -102,16 +103,16 @@ describe('query userHotTakes with upvotes', () => { `; it('should return upvotes count', async () => { - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Popular take', position: 0, }); - await con.getRepository(UserHotTakeUpvote).save([ - { hotTakeId: hotTake.id, userId: '2' }, - { hotTakeId: hotTake.id, userId: '3' }, + await con.getRepository(UserHotTake).save([ + { hotTakeId: hotTake.id, userId: '2', vote: UserVote.Up }, + { hotTakeId: hotTake.id, userId: '3', vote: UserVote.Up }, ]); const res = await client.query(QUERY, { variables: { userId: '1' } }); @@ -119,7 +120,7 @@ describe('query userHotTakes with upvotes', () => { }); it('should return upvoted as null when not logged in', async () => { - await con.getRepository(UserHotTake).save({ + await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -132,16 +133,17 @@ describe('query userHotTakes with upvotes', () => { it('should return upvoted as true when user upvoted', async () => { loggedUser = '2'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', position: 0, }); - await con.getRepository(UserHotTakeUpvote).save({ + await con.getRepository(UserHotTake).save({ hotTakeId: hotTake.id, userId: '2', + vote: UserVote.Up, }); const res = await client.query(QUERY, { variables: { userId: '1' } }); @@ -150,7 +152,7 @@ describe('query userHotTakes with upvotes', () => { it('should return upvoted as false when user has not upvoted', async () => { loggedUser = '2'; - await con.getRepository(UserHotTake).save({ + await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -184,7 +186,7 @@ describe('mutation vote on hot take', () => { it('should upvote a hot take', async () => { loggedUser = '2'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -201,25 +203,27 @@ describe('mutation vote on hot take', () => { expect(res.errors).toBeUndefined(); - const upvote = await con.getRepository(UserHotTakeUpvote).findOneBy({ + const userHotTake = await con.getRepository(UserHotTake).findOneBy({ hotTakeId: hotTake.id, userId: '2', }); - expect(upvote).not.toBeNull(); + expect(userHotTake).not.toBeNull(); + expect(userHotTake?.vote).toBe(UserVote.Up); }); it('should remove upvote when voting with 0', async () => { loggedUser = '2'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', position: 0, }); - await con.getRepository(UserHotTakeUpvote).save({ + await con.getRepository(UserHotTake).save({ hotTakeId: hotTake.id, userId: '2', + vote: UserVote.Up, }); const res = await client.mutate(MUTATION, { @@ -232,16 +236,16 @@ describe('mutation vote on hot take', () => { expect(res.errors).toBeUndefined(); - const upvote = await con.getRepository(UserHotTakeUpvote).findOneBy({ + const userHotTake = await con.getRepository(UserHotTake).findOneBy({ hotTakeId: hotTake.id, userId: '2', }); - expect(upvote).toBeNull(); + expect(userHotTake?.vote).toBe(UserVote.None); }); - it('should not allow downvoting hot takes', async () => { + it('should allow downvoting hot takes', async () => { loggedUser = '2'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -256,7 +260,13 @@ describe('mutation vote on hot take', () => { }, }); - expect(res.errors?.[0]?.message).toBe('Hot takes do not support downvotes'); + expect(res.errors).toBeUndefined(); + + const userHotTake = await con.getRepository(UserHotTake).findOneBy({ + hotTakeId: hotTake.id, + userId: '2', + }); + expect(userHotTake?.vote).toBe(UserVote.Down); }); it('should return error for non-existent hot take', async () => { @@ -274,7 +284,7 @@ describe('mutation vote on hot take', () => { it('should allow upvoting same hot take only once', async () => { loggedUser = '2'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Take', @@ -299,7 +309,7 @@ describe('mutation vote on hot take', () => { expect(res.errors).toBeUndefined(); - const upvotes = await con.getRepository(UserHotTakeUpvote).findBy({ + const upvotes = await con.getRepository(UserHotTake).findBy({ hotTakeId: hotTake.id, userId: '2', }); @@ -358,7 +368,7 @@ describe('mutation addUserHotTake', () => { it('should enforce maximum of 5 hot takes', async () => { loggedUser = '1'; - await con.getRepository(UserHotTake).save([ + await con.getRepository(HotTake).save([ { userId: '1', emoji: '1️⃣', title: 'Take 1', position: 0 }, { userId: '1', emoji: '2️⃣', title: 'Take 2', position: 1 }, { userId: '1', emoji: '3️⃣', title: 'Take 3', position: 2 }, @@ -398,7 +408,7 @@ describe('mutation updateUserHotTake', () => { it('should update hot take', async () => { loggedUser = '1'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Original', @@ -421,7 +431,7 @@ describe('mutation updateUserHotTake', () => { it('should update subtitle', async () => { loggedUser = '1'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'Hot take', @@ -451,7 +461,7 @@ describe('mutation updateUserHotTake', () => { it('should not allow updating other user hot take', async () => { loggedUser = '1'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '2', emoji: '🔥', title: 'Other user take', @@ -486,7 +496,7 @@ describe('mutation deleteUserHotTake', () => { it('should delete hot take', async () => { loggedUser = '1'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '1', emoji: '🔥', title: 'To delete', @@ -496,14 +506,14 @@ describe('mutation deleteUserHotTake', () => { await client.mutate(MUTATION, { variables: { id: hotTake.id } }); const deleted = await con - .getRepository(UserHotTake) + .getRepository(HotTake) .findOneBy({ id: hotTake.id }); expect(deleted).toBeNull(); }); it('should not delete other user hot take', async () => { loggedUser = '1'; - const hotTake = await con.getRepository(UserHotTake).save({ + const hotTake = await con.getRepository(HotTake).save({ userId: '2', emoji: '🔥', title: 'Other user take', @@ -513,7 +523,7 @@ describe('mutation deleteUserHotTake', () => { await client.mutate(MUTATION, { variables: { id: hotTake.id } }); const notDeleted = await con - .getRepository(UserHotTake) + .getRepository(HotTake) .findOneBy({ id: hotTake.id }); expect(notDeleted).not.toBeNull(); }); @@ -538,7 +548,7 @@ describe('mutation reorderUserHotTakes', () => { it('should update positions', async () => { loggedUser = '1'; - const [item1, item2] = await con.getRepository(UserHotTake).save([ + const [item1, item2] = await con.getRepository(HotTake).save([ { userId: '1', emoji: '1️⃣', title: 'Take 1', position: 0 }, { userId: '1', emoji: '2️⃣', title: 'Take 2', position: 1 }, ]); @@ -563,7 +573,7 @@ describe('mutation reorderUserHotTakes', () => { it('should not reorder other user hot takes', async () => { loggedUser = '1'; - const otherUserItem = await con.getRepository(UserHotTake).save({ + const otherUserItem = await con.getRepository(HotTake).save({ userId: '2', emoji: '🔥', title: 'Other user', @@ -577,7 +587,7 @@ describe('mutation reorderUserHotTakes', () => { }); const notUpdated = await con - .getRepository(UserHotTake) + .getRepository(HotTake) .findOneBy({ id: otherUserItem.id }); expect(notUpdated?.position).toBe(0); }); diff --git a/src/common/vote.ts b/src/common/vote.ts index 75dec7f2b7..81173df746 100644 --- a/src/common/vote.ts +++ b/src/common/vote.ts @@ -9,8 +9,8 @@ import { GQLEmptyResponse } from '../schema/common'; import { ensureSourcePermissions } from '../schema/sources'; import { AuthContext } from '../Context'; import { UserComment } from '../entity/user/UserComment'; +import { HotTake } from '../entity/user/HotTake'; import { UserHotTake } from '../entity/user/UserHotTake'; -import { UserHotTakeUpvote } from '../entity/user/UserHotTakeUpvote'; import { UserVote } from '../types'; type UserVoteProps = { @@ -165,34 +165,16 @@ export const voteHotTake = async ({ try { validateVoteType({ vote }); - if (vote === UserVote.Down) { - throw new ValidationError('Hot takes do not support downvotes'); - } - - await ctx.con.transaction(async (manager) => { - const hotTakeRepo = manager.getRepository(UserHotTake); - const upvoteRepo = manager.getRepository(UserHotTakeUpvote); + // Verify hot take exists + await ctx.con.getRepository(HotTake).findOneByOrFail({ id }); - await hotTakeRepo.findOneByOrFail({ id }); + const userHotTakeRepo = ctx.con.getRepository(UserHotTake); - const existingUpvote = await upvoteRepo.findOneBy({ - hotTakeId: id, - userId: ctx.userId, - }); - - if (vote === UserVote.Up && !existingUpvote) { - await upvoteRepo.insert({ - hotTakeId: id, - userId: ctx.userId, - }); - await hotTakeRepo.increment({ id }, 'upvotes', 1); - } else if (vote === UserVote.None && existingUpvote) { - await upvoteRepo.delete({ - hotTakeId: id, - userId: ctx.userId, - }); - await hotTakeRepo.decrement({ id }, 'upvotes', 1); - } + // Save vote (triggers handle upvotes count updates) + await userHotTakeRepo.save({ + hotTakeId: id, + userId: ctx.userId, + vote, }); } catch (originalError) { const err = originalError as TypeORMQueryFailedError; diff --git a/src/entity/user/HotTake.ts b/src/entity/user/HotTake.ts new file mode 100644 index 0000000000..9fd130dac3 --- /dev/null +++ b/src/entity/user/HotTake.ts @@ -0,0 +1,49 @@ +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import type { User } from './User'; + +@Entity() +@Index('IDX_hot_take_user_id', ['userId']) +export class HotTake { + @PrimaryGeneratedColumn('uuid', { + primaryKeyConstraintName: 'PK_hot_take_id', + }) + id: string; + + @Column({ type: 'text' }) + userId: string; + + @Column({ type: 'text' }) + emoji: string; + + @Column({ type: 'text' }) + title: string; + + @Column({ type: 'text', nullable: true }) + subtitle: string | null; + + @Column({ type: 'integer' }) + position: number; + + @Column({ type: 'integer', default: 0 }) + upvotes: number; + + @Column({ type: 'timestamp', default: () => 'now()' }) + createdAt: Date; + + @ManyToOne('User', { + lazy: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'userId', + foreignKeyConstraintName: 'FK_hot_take_user_id', + }) + user: Promise; +} diff --git a/src/entity/user/UserHotTake.ts b/src/entity/user/UserHotTake.ts index b3fab490dd..ea6d63ad52 100644 --- a/src/entity/user/UserHotTake.ts +++ b/src/entity/user/UserHotTake.ts @@ -1,41 +1,48 @@ import { Column, + CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, - PrimaryGeneratedColumn, + PrimaryColumn, + UpdateDateColumn, } from 'typeorm'; import type { User } from './User'; +import type { HotTake } from './HotTake'; +import { UserVote } from '../../types'; @Entity() -@Index('IDX_user_hot_take_user_id', ['userId']) +@Index(['hotTakeId', 'userId'], { unique: true }) +@Index(['userId', 'vote', 'votedAt']) export class UserHotTake { - @PrimaryGeneratedColumn('uuid', { - primaryKeyConstraintName: 'PK_user_hot_take_id', - }) - id: string; + @PrimaryColumn({ type: 'uuid' }) + hotTakeId: string; - @Column({ type: 'text' }) + @PrimaryColumn({ type: 'text' }) userId: string; - @Column({ type: 'text' }) - emoji: string; - - @Column({ type: 'text' }) - title: string; + @CreateDateColumn() + createdAt: Date; - @Column({ type: 'text', nullable: true }) - subtitle: string | null; + @UpdateDateColumn() + updatedAt: Date; - @Column({ type: 'integer' }) - position: number; + @Column({ default: null, nullable: true }) + votedAt: Date; - @Column({ type: 'integer', default: 0 }) - upvotes: number; + @Column({ type: 'smallint', default: UserVote.None }) + vote: UserVote = UserVote.None; - @Column({ type: 'timestamp', default: () => 'now()' }) - createdAt: Date; + @ManyToOne('HotTake', { + lazy: true, + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'hotTakeId', + foreignKeyConstraintName: 'FK_user_hot_take_hot_take_id', + }) + hotTake: Promise; @ManyToOne('User', { lazy: true, diff --git a/src/entity/user/UserHotTakeUpvote.ts b/src/entity/user/UserHotTakeUpvote.ts deleted file mode 100644 index 2987ab7356..0000000000 --- a/src/entity/user/UserHotTakeUpvote.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - CreateDateColumn, - Entity, - Index, - JoinColumn, - ManyToOne, - PrimaryColumn, -} from 'typeorm'; -import type { User } from './User'; -import type { UserHotTake } from './UserHotTake'; - -@Entity() -@Index(['hotTakeId', 'userId'], { unique: true }) -@Index(['userId', 'createdAt']) -@Index(['hotTakeId', 'createdAt']) -export class UserHotTakeUpvote { - @PrimaryColumn({ type: 'uuid' }) - hotTakeId: string; - - @PrimaryColumn({ type: 'text' }) - userId: string; - - @CreateDateColumn() - createdAt: Date; - - @ManyToOne('UserHotTake', { - lazy: true, - onDelete: 'CASCADE', - }) - @JoinColumn({ - name: 'hotTakeId', - foreignKeyConstraintName: 'FK_user_hot_take_upvote_hot_take_id', - }) - hotTake: Promise; - - @ManyToOne('User', { - lazy: true, - onDelete: 'CASCADE', - }) - @JoinColumn({ - name: 'userId', - foreignKeyConstraintName: 'FK_user_hot_take_upvote_user_id', - }) - user: Promise; -} diff --git a/src/entity/user/index.ts b/src/entity/user/index.ts index 834cd04e51..2949ebe2d6 100644 --- a/src/entity/user/index.ts +++ b/src/entity/user/index.ts @@ -8,8 +8,8 @@ export * from './UserMarketingCta'; export * from './UserStats'; export * from './UserTopReader'; export * from './UserStack'; +export * from './HotTake'; export * from './UserHotTake'; -export * from './UserHotTakeUpvote'; export * from './UserWorkspacePhoto'; export * from './UserGear'; export * from './UserTool'; diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index a98ba66c39..c70504cad4 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -2203,11 +2203,13 @@ const obj = new GraphORM({ }, }, }, - UserHotTake: { + HotTake: { requiredColumns: ['id', 'userId'], fields: { upvoted: { - select: existsByUserAndHotTake('UserHotTakeUpvote'), + select: existsByUserAndHotTake('UserHotTake', (qb) => + qb.andWhere(`${qb.alias}.vote = 1`), + ), transform: nullIfNotLoggedIn, }, createdAt: { diff --git a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts index 2e13dbed13..467d0c23f8 100644 --- a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts +++ b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts @@ -1,62 +1,147 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddUserHotTakeUpvotes1769156534090 implements MigrationInterface { - name = 'AddUserHotTakeUpvotes1769156534090'; +export class HotTakeVoting1769156534090 implements MigrationInterface { + name = 'HotTakeVoting1769156534090'; public async up(queryRunner: QueryRunner): Promise { + // Rename user_hot_take to hot_take + await queryRunner.query(`ALTER TABLE "user_hot_take" RENAME TO "hot_take"`); + await queryRunner.query(`ALTER INDEX "IDX_user_hot_take_user_id" RENAME TO "IDX_hot_take_user_id"`); + await queryRunner.query(`ALTER TABLE "hot_take" RENAME CONSTRAINT "PK_user_hot_take_id" TO "PK_hot_take_id"`); + await queryRunner.query(`ALTER TABLE "hot_take" RENAME CONSTRAINT "FK_user_hot_take_user_id" TO "FK_hot_take_user_id"`); + + // Create new user_hot_take table (like user_post) await queryRunner.query(` - CREATE TABLE "user_hot_take_upvote" ( + CREATE TABLE "user_hot_take" ( "hotTakeId" uuid NOT NULL, "userId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), - CONSTRAINT "PK_0fb6851f42e1d3f4138e3e4b8ca" PRIMARY KEY ("hotTakeId", "userId") + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + "votedAt" TIMESTAMP, + "vote" smallint NOT NULL DEFAULT 0, + CONSTRAINT "PK_user_hot_take" PRIMARY KEY ("hotTakeId", "userId") ) `); + await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_hotTakeId_userId" ON "user_hot_take" ("hotTakeId", "userId")`); + await queryRunner.query(`CREATE INDEX "IDX_user_hot_take_userId_vote_votedAt" ON "user_hot_take" ("userId", "vote", "votedAt")`); + await queryRunner.query(` - CREATE INDEX "IDX_902a36de7917e47ab8839ae46f" ON "user_hot_take_upvote" ("hotTakeId", "createdAt") - `); - await queryRunner.query(` - CREATE INDEX "IDX_cc451486cbf598411e27264102" ON "user_hot_take_upvote" ("userId", "createdAt") + ALTER TABLE "user_hot_take" + ADD CONSTRAINT "FK_user_hot_take_hot_take_id" + FOREIGN KEY ("hotTakeId") + REFERENCES "hot_take"("id") + ON DELETE CASCADE `); await queryRunner.query(` - ALTER TABLE "user_hot_take" ADD "upvotes" integer NOT NULL DEFAULT 0 + ALTER TABLE "user_hot_take" + ADD CONSTRAINT "FK_user_hot_take_user_id" + FOREIGN KEY ("userId") + REFERENCES "user"("id") + ON DELETE CASCADE `); + + // Create votedAt trigger function await queryRunner.query(` - ALTER TABLE "user_hot_take_upvote" - ADD CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" - FOREIGN KEY ("hotTakeId") - REFERENCES "user_hot_take"("id") - ON DELETE CASCADE - ON UPDATE NO ACTION + CREATE OR REPLACE FUNCTION hot_take_voted_at_time() + RETURNS TRIGGER AS $$ + BEGIN + NEW."votedAt" = now(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; `); await queryRunner.query(` - ALTER TABLE "user_hot_take_upvote" - ADD CONSTRAINT "FK_user_hot_take_upvote_user_id" - FOREIGN KEY ("userId") - REFERENCES "user"("id") - ON DELETE CASCADE - ON UPDATE NO ACTION + CREATE TRIGGER user_hot_take_voted_at_trigger + BEFORE INSERT OR UPDATE ON user_hot_take + FOR EACH ROW + WHEN (NEW.vote IS DISTINCT FROM 0) + EXECUTE FUNCTION hot_take_voted_at_time(); `); - } - public async down(queryRunner: QueryRunner): Promise { + // Create vote insert trigger await queryRunner.query(` - ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_user_id" + CREATE OR REPLACE FUNCTION user_hot_take_vote_insert_trigger_function() + RETURNS TRIGGER AS $$ + BEGIN + IF NEW.vote = 1 THEN + UPDATE hot_take SET upvotes = upvotes + 1 WHERE id = NEW."hotTakeId"; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql; `); await queryRunner.query(` - ALTER TABLE "user_hot_take_upvote" DROP CONSTRAINT "FK_user_hot_take_upvote_hot_take_id" + CREATE TRIGGER user_hot_take_vote_insert_trigger + AFTER INSERT ON user_hot_take + FOR EACH ROW + EXECUTE FUNCTION user_hot_take_vote_insert_trigger_function(); `); + + // Create vote update trigger await queryRunner.query(` - ALTER TABLE "user_hot_take" DROP COLUMN "upvotes" + CREATE OR REPLACE FUNCTION user_hot_take_vote_update_trigger_function() + RETURNS TRIGGER AS $$ + BEGIN + IF OLD.vote IS DISTINCT FROM NEW.vote THEN + IF OLD.vote = 0 AND NEW.vote = 1 THEN + UPDATE hot_take SET upvotes = upvotes + 1 WHERE id = NEW."hotTakeId"; + ELSIF OLD.vote = 1 AND NEW.vote = 0 THEN + UPDATE hot_take SET upvotes = upvotes - 1 WHERE id = NEW."hotTakeId"; + END IF; + END IF; + RETURN NEW; + END; + $$ LANGUAGE plpgsql; `); await queryRunner.query(` - DROP INDEX "public"."IDX_cc451486cbf598411e27264102" + CREATE TRIGGER user_hot_take_vote_update_trigger + AFTER UPDATE ON user_hot_take + FOR EACH ROW + EXECUTE FUNCTION user_hot_take_vote_update_trigger_function(); `); + + // Create vote delete trigger await queryRunner.query(` - DROP INDEX "public"."IDX_902a36de7917e47ab8839ae46f" + CREATE OR REPLACE FUNCTION user_hot_take_vote_delete_trigger_function() + RETURNS TRIGGER AS $$ + BEGIN + IF OLD.vote = 1 THEN + UPDATE hot_take SET upvotes = upvotes - 1 WHERE id = OLD."hotTakeId"; + END IF; + RETURN OLD; + END; + $$ LANGUAGE plpgsql; `); await queryRunner.query(` - DROP TABLE "user_hot_take_upvote" + CREATE TRIGGER user_hot_take_vote_delete_trigger + AFTER DELETE ON user_hot_take + FOR EACH ROW + EXECUTE FUNCTION user_hot_take_vote_delete_trigger_function(); `); } + + public async down(queryRunner: QueryRunner): Promise { + // Drop triggers + await queryRunner.query('DROP TRIGGER IF EXISTS user_hot_take_vote_delete_trigger ON user_hot_take'); + await queryRunner.query('DROP FUNCTION IF EXISTS user_hot_take_vote_delete_trigger_function'); + await queryRunner.query('DROP TRIGGER IF EXISTS user_hot_take_vote_update_trigger ON user_hot_take'); + await queryRunner.query('DROP FUNCTION IF EXISTS user_hot_take_vote_update_trigger_function'); + await queryRunner.query('DROP TRIGGER IF EXISTS user_hot_take_vote_insert_trigger ON user_hot_take'); + await queryRunner.query('DROP FUNCTION IF EXISTS user_hot_take_vote_insert_trigger_function'); + await queryRunner.query('DROP TRIGGER IF EXISTS user_hot_take_voted_at_trigger ON user_hot_take'); + await queryRunner.query('DROP FUNCTION IF EXISTS hot_take_voted_at_time'); + + // Drop user_hot_take table + await queryRunner.query('ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_user_id"'); + await queryRunner.query('ALTER TABLE "user_hot_take" DROP CONSTRAINT "FK_user_hot_take_hot_take_id"'); + await queryRunner.query('DROP INDEX "IDX_user_hot_take_userId_vote_votedAt"'); + await queryRunner.query('DROP INDEX "IDX_user_hot_take_hotTakeId_userId"'); + await queryRunner.query('DROP TABLE "user_hot_take"'); + + // Rename hot_take back to user_hot_take + await queryRunner.query('ALTER TABLE "hot_take" RENAME CONSTRAINT "FK_hot_take_user_id" TO "FK_user_hot_take_user_id"'); + await queryRunner.query('ALTER TABLE "hot_take" RENAME CONSTRAINT "PK_hot_take_id" TO "PK_user_hot_take_id"'); + await queryRunner.query('ALTER INDEX "IDX_hot_take_user_id" RENAME TO "IDX_user_hot_take_user_id"'); + await queryRunner.query('ALTER TABLE "hot_take" RENAME TO "user_hot_take"'); + } } diff --git a/src/schema/userHotTake.ts b/src/schema/userHotTake.ts index 7c70a63415..505b369e0c 100644 --- a/src/schema/userHotTake.ts +++ b/src/schema/userHotTake.ts @@ -3,7 +3,7 @@ import { traceResolvers } from './trace'; import { AuthContext, BaseContext, Context } from '../Context'; import graphorm from '../graphorm'; import { offsetPageGenerator, GQLEmptyResponse } from './common'; -import { UserHotTake } from '../entity/user/UserHotTake'; +import { HotTake } from '../entity/user/HotTake'; import { ValidationError } from 'apollo-server-errors'; import { addUserHotTakeSchema, @@ -15,7 +15,7 @@ import { } from '../common/schema/userHotTake'; import { NEW_ITEM_POSITION } from '../common/constants'; -interface GQLUserHotTake { +interface GQLHotTake { id: string; userId: string; emoji: string; @@ -28,7 +28,7 @@ interface GQLUserHotTake { const MAX_HOT_TAKES = 5; export const typeDefs = /* GraphQL */ ` - type UserHotTake { + type HotTake { id: ID! emoji: String! title: String! @@ -39,29 +39,29 @@ export const typeDefs = /* GraphQL */ ` createdAt: DateTime! } - type UserHotTakeEdge { - node: UserHotTake! + type HotTakeEdge { + node: HotTake! cursor: String! } - type UserHotTakeConnection { + type HotTakeConnection { pageInfo: PageInfo! - edges: [UserHotTakeEdge!]! + edges: [HotTakeEdge!]! } - input AddUserHotTakeInput { + input AddHotTakeInput { emoji: String! title: String! subtitle: String } - input UpdateUserHotTakeInput { + input UpdateHotTakeInput { emoji: String title: String subtitle: String } - input ReorderUserHotTakeInput { + input ReorderHotTakeInput { id: ID! position: Int! } @@ -70,31 +70,29 @@ export const typeDefs = /* GraphQL */ ` """ Get a user's hot takes """ - userHotTakes(userId: ID!, first: Int, after: String): UserHotTakeConnection! + hotTakes(userId: ID!, first: Int, after: String): HotTakeConnection! } extend type Mutation { """ Add a hot take to the user's profile (max 5) """ - addUserHotTake(input: AddUserHotTakeInput!): UserHotTake! @auth + addHotTake(input: AddHotTakeInput!): HotTake! @auth """ - Update a user's hot take + Update a hot take """ - updateUserHotTake(id: ID!, input: UpdateUserHotTakeInput!): UserHotTake! - @auth + updateHotTake(id: ID!, input: UpdateHotTakeInput!): HotTake! @auth """ - Delete a user's hot take + Delete a hot take """ - deleteUserHotTake(id: ID!): EmptyResponse! @auth + deleteHotTake(id: ID!): EmptyResponse! @auth """ - Reorder user's hot takes + Reorder hot takes """ - reorderUserHotTakes(items: [ReorderUserHotTakeInput!]!): [UserHotTake!]! - @auth + reorderHotTakes(items: [ReorderHotTakeInput!]!): [HotTake!]! @auth } `; @@ -103,19 +101,19 @@ export const resolvers: IResolvers = traceResolvers< BaseContext >({ Query: { - userHotTakes: async ( + hotTakes: async ( _, args: { userId: string; first?: number; after?: string }, ctx: Context, info, ) => { - const pageGenerator = offsetPageGenerator(50, 100); + const pageGenerator = offsetPageGenerator(50, 100); const page = pageGenerator.connArgsToPage({ first: args.first, after: args.after, }); - return graphorm.queryPaginated( + return graphorm.queryPaginated( ctx, info, (nodeSize) => pageGenerator.hasPreviousPage(page, nodeSize), @@ -140,7 +138,7 @@ export const resolvers: IResolvers = traceResolvers< }, Mutation: { - addUserHotTake: async ( + addHotTake: async ( _, args: { input: AddUserHotTakeInput }, ctx: AuthContext, @@ -148,7 +146,7 @@ export const resolvers: IResolvers = traceResolvers< ) => { const input = addUserHotTakeSchema.parse(args.input); - const count = await ctx.con.getRepository(UserHotTake).count({ + const count = await ctx.con.getRepository(HotTake).count({ where: { userId: ctx.userId }, }); @@ -158,7 +156,7 @@ export const resolvers: IResolvers = traceResolvers< ); } - const hotTake = ctx.con.getRepository(UserHotTake).create({ + const hotTake = ctx.con.getRepository(HotTake).create({ userId: ctx.userId, emoji: input.emoji, title: input.title, @@ -166,7 +164,7 @@ export const resolvers: IResolvers = traceResolvers< position: NEW_ITEM_POSITION, }); - await ctx.con.getRepository(UserHotTake).save(hotTake); + await ctx.con.getRepository(HotTake).save(hotTake); return graphorm.queryOneOrFail(ctx, info, (builder) => { builder.queryBuilder.where(`"${builder.alias}"."id" = :id`, { @@ -176,7 +174,7 @@ export const resolvers: IResolvers = traceResolvers< }); }, - updateUserHotTake: async ( + updateHotTake: async ( _, args: { id: string; input: UpdateUserHotTakeInput }, ctx: AuthContext, @@ -184,7 +182,7 @@ export const resolvers: IResolvers = traceResolvers< ) => { const input = updateUserHotTakeSchema.parse(args.input); - const hotTake = await ctx.con.getRepository(UserHotTake).findOne({ + const hotTake = await ctx.con.getRepository(HotTake).findOne({ where: { id: args.id, userId: ctx.userId }, }); @@ -192,7 +190,7 @@ export const resolvers: IResolvers = traceResolvers< throw new ValidationError('Hot take not found'); } - const updateData: Partial = {}; + const updateData: Partial = {}; if (input.emoji !== undefined) { updateData.emoji = input.emoji; } @@ -205,7 +203,7 @@ export const resolvers: IResolvers = traceResolvers< if (Object.keys(updateData).length > 0) { await ctx.con - .getRepository(UserHotTake) + .getRepository(HotTake) .update({ id: args.id }, updateData); } @@ -217,19 +215,19 @@ export const resolvers: IResolvers = traceResolvers< }); }, - deleteUserHotTake: async ( + deleteHotTake: async ( _, args: { id: string }, ctx: AuthContext, ): Promise => { await ctx.con - .getRepository(UserHotTake) + .getRepository(HotTake) .delete({ id: args.id, userId: ctx.userId }); return { _: true }; }, - reorderUserHotTakes: async ( + reorderHotTakes: async ( _, args: { items: ReorderUserHotTakeInput[] }, ctx: AuthContext, @@ -243,7 +241,7 @@ export const resolvers: IResolvers = traceResolvers< .join(' '); await ctx.con - .getRepository(UserHotTake) + .getRepository(HotTake) .createQueryBuilder() .update() .set({ position: () => `CASE ${whenClauses} ELSE position END` }) From cc33ed9085c461d23a5f6dfc70f92d51454b4aae Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 13:02:47 +0200 Subject: [PATCH 6/7] refactor: rename test file to hotTake.ts and update GraphQL names Update test queries/mutations to use new names (hotTakes, addHotTake, etc.) Co-Authored-By: Claude Opus 4.5 --- __tests__/{userHotTake.ts => hotTake.ts} | 64 ++++++++++++------------ 1 file changed, 32 insertions(+), 32 deletions(-) rename __tests__/{userHotTake.ts => hotTake.ts} (88%) diff --git a/__tests__/userHotTake.ts b/__tests__/hotTake.ts similarity index 88% rename from __tests__/userHotTake.ts rename to __tests__/hotTake.ts index cc03dbbb64..41432e1494 100644 --- a/__tests__/userHotTake.ts +++ b/__tests__/hotTake.ts @@ -34,10 +34,10 @@ beforeEach(async () => { await saveFixtures(con, User, usersFixture); }); -describe('query userHotTakes', () => { +describe('query hotTakes', () => { const QUERY = ` - query UserHotTakes($userId: ID!) { - userHotTakes(userId: $userId) { + query HotTakes($userId: ID!) { + hotTakes(userId: $userId) { edges { node { id @@ -53,7 +53,7 @@ describe('query userHotTakes', () => { it('should return empty list for user with no hot takes', async () => { const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges).toEqual([]); + expect(res.data.hotTakes.edges).toEqual([]); }); it('should return hot takes ordered by position', async () => { @@ -63,9 +63,9 @@ describe('query userHotTakes', () => { ]); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges).toHaveLength(2); - expect(res.data.userHotTakes.edges[0].node.title).toBe('Hot take 2'); - expect(res.data.userHotTakes.edges[1].node.title).toBe('Hot take 1'); + expect(res.data.hotTakes.edges).toHaveLength(2); + expect(res.data.hotTakes.edges[0].node.title).toBe('Hot take 2'); + expect(res.data.hotTakes.edges[1].node.title).toBe('Hot take 1'); }); it('should return hot takes with subtitle', async () => { @@ -78,7 +78,7 @@ describe('query userHotTakes', () => { }); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges[0].node).toMatchObject({ + expect(res.data.hotTakes.edges[0].node).toMatchObject({ emoji: '🎯', title: 'My opinion', subtitle: 'Some context', @@ -86,10 +86,10 @@ describe('query userHotTakes', () => { }); }); -describe('query userHotTakes with upvotes', () => { +describe('query hotTakes with upvotes', () => { const QUERY = ` - query UserHotTakes($userId: ID!) { - userHotTakes(userId: $userId) { + query HotTakes($userId: ID!) { + hotTakes(userId: $userId) { edges { node { id @@ -116,7 +116,7 @@ describe('query userHotTakes with upvotes', () => { ]); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges[0].node.upvotes).toBe(2); + expect(res.data.hotTakes.edges[0].node.upvotes).toBe(2); }); it('should return upvoted as null when not logged in', async () => { @@ -128,7 +128,7 @@ describe('query userHotTakes with upvotes', () => { }); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges[0].node.upvoted).toBeNull(); + expect(res.data.hotTakes.edges[0].node.upvoted).toBeNull(); }); it('should return upvoted as true when user upvoted', async () => { @@ -147,7 +147,7 @@ describe('query userHotTakes with upvotes', () => { }); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges[0].node.upvoted).toBe(true); + expect(res.data.hotTakes.edges[0].node.upvoted).toBe(true); }); it('should return upvoted as false when user has not upvoted', async () => { @@ -160,7 +160,7 @@ describe('query userHotTakes with upvotes', () => { }); const res = await client.query(QUERY, { variables: { userId: '1' } }); - expect(res.data.userHotTakes.edges[0].node.upvoted).toBe(false); + expect(res.data.hotTakes.edges[0].node.upvoted).toBe(false); }); }); @@ -317,10 +317,10 @@ describe('mutation vote on hot take', () => { }); }); -describe('mutation addUserHotTake', () => { +describe('mutation addHotTake', () => { const MUTATION = ` - mutation AddUserHotTake($input: AddUserHotTakeInput!) { - addUserHotTake(input: $input) { + mutation AddHotTake($input: AddHotTakeInput!) { + addHotTake(input: $input) { id emoji title @@ -344,7 +344,7 @@ describe('mutation addUserHotTake', () => { }); expect(res.errors).toBeUndefined(); - expect(res.data.addUserHotTake).toMatchObject({ + expect(res.data.addHotTake).toMatchObject({ emoji: '🔥', title: 'Hot take', subtitle: null, @@ -359,7 +359,7 @@ describe('mutation addUserHotTake', () => { }, }); - expect(res.data.addUserHotTake).toMatchObject({ + expect(res.data.addHotTake).toMatchObject({ emoji: '💡', title: 'Idea', subtitle: 'Explanation', @@ -384,10 +384,10 @@ describe('mutation addUserHotTake', () => { }); }); -describe('mutation updateUserHotTake', () => { +describe('mutation updateHotTake', () => { const MUTATION = ` - mutation UpdateUserHotTake($id: ID!, $input: UpdateUserHotTakeInput!) { - updateUserHotTake(id: $id, input: $input) { + mutation UpdateHotTake($id: ID!, $input: UpdateHotTakeInput!) { + updateHotTake(id: $id, input: $input) { id emoji title @@ -423,7 +423,7 @@ describe('mutation updateUserHotTake', () => { }); expect(res.errors).toBeUndefined(); - expect(res.data.updateUserHotTake).toMatchObject({ + expect(res.data.updateHotTake).toMatchObject({ emoji: '💯', title: 'Updated', }); @@ -445,7 +445,7 @@ describe('mutation updateUserHotTake', () => { }, }); - expect(res.data.updateUserHotTake.subtitle).toBe('New context'); + expect(res.data.updateHotTake.subtitle).toBe('New context'); }); it('should return error for non-existent item', async () => { @@ -478,10 +478,10 @@ describe('mutation updateUserHotTake', () => { }); }); -describe('mutation deleteUserHotTake', () => { +describe('mutation deleteHotTake', () => { const MUTATION = ` - mutation DeleteUserHotTake($id: ID!) { - deleteUserHotTake(id: $id) { + mutation DeleteHotTake($id: ID!) { + deleteHotTake(id: $id) { _ } } @@ -529,10 +529,10 @@ describe('mutation deleteUserHotTake', () => { }); }); -describe('mutation reorderUserHotTakes', () => { +describe('mutation reorderHotTakes', () => { const MUTATION = ` - mutation ReorderUserHotTakes($items: [ReorderUserHotTakeInput!]!) { - reorderUserHotTakes(items: $items) { + mutation ReorderHotTakes($items: [ReorderHotTakeInput!]!) { + reorderHotTakes(items: $items) { id position } @@ -562,7 +562,7 @@ describe('mutation reorderUserHotTakes', () => { }, }); - const reordered = res.data.reorderUserHotTakes; + const reordered = res.data.reorderHotTakes; expect( reordered.find((i: { id: string }) => i.id === item1.id).position, ).toBe(1); From c52cb47de571bb8e5f72c7ec34b9ba35b218320c Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Fri, 23 Jan 2026 14:04:58 +0200 Subject: [PATCH 7/7] fix: migration --- src/graphorm/index.ts | 2 +- src/migration/1769156534090-AddUserHotTakeUpvotes.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/graphorm/index.ts b/src/graphorm/index.ts index c70504cad4..6aff122c4e 100644 --- a/src/graphorm/index.ts +++ b/src/graphorm/index.ts @@ -113,7 +113,7 @@ const existsByUserAndHotTake = let query = qb .select('1') .from(entity, 'a') - .where(`a."userId" = :userId`, { userId: ctx.userId }) + .where(`a."userId" = :upvoterUserId`, { upvoterUserId: ctx.userId }) .andWhere(`a."hotTakeId" = ${alias}.id`) .limit(1); diff --git a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts index 467d0c23f8..470349fe7f 100644 --- a/src/migration/1769156534090-AddUserHotTakeUpvotes.ts +++ b/src/migration/1769156534090-AddUserHotTakeUpvotes.ts @@ -9,6 +9,7 @@ export class HotTakeVoting1769156534090 implements MigrationInterface { await queryRunner.query(`ALTER INDEX "IDX_user_hot_take_user_id" RENAME TO "IDX_hot_take_user_id"`); await queryRunner.query(`ALTER TABLE "hot_take" RENAME CONSTRAINT "PK_user_hot_take_id" TO "PK_hot_take_id"`); await queryRunner.query(`ALTER TABLE "hot_take" RENAME CONSTRAINT "FK_user_hot_take_user_id" TO "FK_hot_take_user_id"`); + await queryRunner.query(`ALTER TABLE "hot_take" ADD COLUMN "upvotes" integer NOT NULL DEFAULT 0`); // Create new user_hot_take table (like user_post) await queryRunner.query(` @@ -142,6 +143,7 @@ export class HotTakeVoting1769156534090 implements MigrationInterface { await queryRunner.query('ALTER TABLE "hot_take" RENAME CONSTRAINT "FK_hot_take_user_id" TO "FK_user_hot_take_user_id"'); await queryRunner.query('ALTER TABLE "hot_take" RENAME CONSTRAINT "PK_hot_take_id" TO "PK_user_hot_take_id"'); await queryRunner.query('ALTER INDEX "IDX_hot_take_user_id" RENAME TO "IDX_user_hot_take_user_id"'); + await queryRunner.query(`ALTER TABLE "hot_take" DROP COLUMN "upvotes"`); await queryRunner.query('ALTER TABLE "hot_take" RENAME TO "user_hot_take"'); } }