Skip to content

Commit 035ea10

Browse files
Fix/hashtag count (#187)
* fix(hashtags): remove created by column * feat(hashtag): add tweet hashtag entity * feat(trend): disable fake trends for now * feat(hashtags): add trigger for hashtag count * feat(hashtags): extract hashtags from quotes and replies * fix(db): remove view migrations * fix(db): copy migrations file to database folder * test(hashtag): fix unit tests --------- Co-authored-by: Mario Raafat <136023677+MarioRaafat@users.noreply.github.com>
1 parent 716c75d commit 035ea10

15 files changed

+297
-20
lines changed

src/databases/data-source.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Chat } from '../chat/entities/chat.entity';
2323
import { Message } from '../messages/entities/message.entity';
2424
import { MessageReaction } from '../messages/entities/message-reaction.entity';
2525
import { readFileSync } from 'fs';
26+
import { TweetHashtag } from '../tweets/entities/tweet-hashtag.entity';
2627

2728
config({ path: resolve(__dirname, '../../config/.env') });
2829

@@ -76,6 +77,7 @@ const base_config: any = {
7677
Message,
7778
MessageReaction,
7879
TweetSummary,
80+
TweetHashtag,
7981
],
8082

8183
migrations: [__dirname + '/../migrations/*{.ts,.js}'],
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class CreateHashtagCleanupTrigger1765394569999 implements MigrationInterface {
4+
public async up(query_runner: QueryRunner): Promise<void> {
5+
// Create function to cleanup hashtags when tweet is deleted
6+
await query_runner.query(`
7+
CREATE OR REPLACE FUNCTION cleanup_hashtags_on_tweet_delete()
8+
RETURNS TRIGGER AS $$
9+
BEGIN
10+
-- Decrement usage_count for all hashtags associated with the deleted tweet
11+
UPDATE hashtag
12+
SET usage_count = usage_count - 1
13+
WHERE name IN (
14+
SELECT hashtag_name
15+
FROM tweet_hashtags
16+
WHERE tweet_id = OLD.tweet_id
17+
);
18+
19+
-- Delete hashtags with usage_count <= 0
20+
DELETE FROM hashtag
21+
WHERE usage_count <= 0;
22+
23+
RETURN OLD;
24+
END;
25+
$$ LANGUAGE plpgsql;
26+
`);
27+
28+
// Create trigger that fires BEFORE DELETE on tweet table
29+
await query_runner.query(`
30+
CREATE TRIGGER tweet_delete_hashtag_cleanup_trigger
31+
BEFORE DELETE ON "tweets"
32+
FOR EACH ROW
33+
EXECUTE FUNCTION cleanup_hashtags_on_tweet_delete();
34+
`);
35+
}
36+
37+
public async down(query_runner: QueryRunner): Promise<void> {
38+
// Drop trigger
39+
await query_runner.query(`
40+
DROP TRIGGER IF EXISTS tweet_delete_hashtag_cleanup_trigger ON "tweets"
41+
`);
42+
43+
// Drop function
44+
await query_runner.query(`
45+
DROP FUNCTION IF EXISTS cleanup_hashtags_on_tweet_delete()
46+
`);
47+
}
48+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class RemoveCreatedBy1765557470457 implements MigrationInterface {
4+
name = 'RemoveCreatedBy1765557470457';
5+
6+
public async up(query_runner: QueryRunner): Promise<void> {
7+
await query_runner.query(
8+
`ALTER TABLE "hashtag" DROP CONSTRAINT "FK_11c8b3519f62b36dd5385c217d3"`
9+
);
10+
11+
await query_runner.query(`ALTER TABLE "hashtag" DROP COLUMN "created_by"`);
12+
}
13+
14+
public async down(query_runner: QueryRunner): Promise<void> {
15+
await query_runner.query(`ALTER TABLE "hashtag" ADD "created_by" uuid`);
16+
17+
await query_runner.query(
18+
`ALTER TABLE "hashtag" ADD CONSTRAINT "FK_11c8b3519f62b36dd5385c217d3" FOREIGN KEY ("created_by") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
19+
);
20+
}
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class TweetHashtagEntity1765585636405 implements MigrationInterface {
4+
name = 'TweetHashtagEntity1765585636405';
5+
6+
public async up(query_runner: QueryRunner): Promise<void> {
7+
await query_runner.query(
8+
`CREATE TABLE "tweet_hashtags" ("tweet_id" uuid NOT NULL, "hashtag_name" character varying NOT NULL, CONSTRAINT "PK_42219b0e52e3bee49d2772b3a54" PRIMARY KEY ("tweet_id", "hashtag_name"))`
9+
);
10+
11+
await query_runner.query(
12+
`ALTER TABLE "tweet_hashtags" ADD CONSTRAINT "FK_efe191c9c3d1359e60bac167736" FOREIGN KEY ("tweet_id") REFERENCES "tweets"("tweet_id") ON DELETE CASCADE ON UPDATE NO ACTION`
13+
);
14+
await query_runner.query(
15+
`ALTER TABLE "tweet_hashtags" ADD CONSTRAINT "FK_b0a40275de4a8088c5e6426419d" FOREIGN KEY ("hashtag_name") REFERENCES "hashtag"("name") ON DELETE CASCADE ON UPDATE NO ACTION`
16+
);
17+
}
18+
19+
public async down(query_runner: QueryRunner): Promise<void> {
20+
await query_runner.query(
21+
`ALTER TABLE "tweet_hashtags" DROP CONSTRAINT "FK_b0a40275de4a8088c5e6426419d"`
22+
);
23+
await query_runner.query(
24+
`ALTER TABLE "tweet_hashtags" DROP CONSTRAINT "FK_efe191c9c3d1359e60bac167736"`
25+
);
26+
27+
await query_runner.query(`DROP TABLE "tweet_hashtags"`);
28+
}
29+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class CreateHashtagCleanupTrigger1765394569999 implements MigrationInterface {
4+
public async up(query_runner: QueryRunner): Promise<void> {
5+
// Create function to cleanup hashtags when tweet is deleted
6+
await query_runner.query(`
7+
CREATE OR REPLACE FUNCTION cleanup_hashtags_on_tweet_delete()
8+
RETURNS TRIGGER AS $$
9+
BEGIN
10+
-- Decrement usage_count for all hashtags associated with the deleted tweet
11+
UPDATE hashtag
12+
SET usage_count = usage_count - 1
13+
WHERE name IN (
14+
SELECT hashtag_name
15+
FROM tweet_hashtags
16+
WHERE tweet_id = OLD.tweet_id
17+
);
18+
19+
-- Delete hashtags with usage_count <= 0
20+
DELETE FROM hashtag
21+
WHERE usage_count <= 0;
22+
23+
RETURN OLD;
24+
END;
25+
$$ LANGUAGE plpgsql;
26+
`);
27+
28+
// Create trigger that fires BEFORE DELETE on tweet table
29+
await query_runner.query(`
30+
CREATE TRIGGER tweet_delete_hashtag_cleanup_trigger
31+
BEFORE DELETE ON "tweets"
32+
FOR EACH ROW
33+
EXECUTE FUNCTION cleanup_hashtags_on_tweet_delete();
34+
`);
35+
}
36+
37+
public async down(query_runner: QueryRunner): Promise<void> {
38+
// Drop trigger
39+
await query_runner.query(`
40+
DROP TRIGGER IF EXISTS tweet_delete_hashtag_cleanup_trigger ON "tweets"
41+
`);
42+
43+
// Drop function
44+
await query_runner.query(`
45+
DROP FUNCTION IF EXISTS cleanup_hashtags_on_tweet_delete()
46+
`);
47+
}
48+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class RemoveCreatedBy1765557470457 implements MigrationInterface {
4+
name = 'RemoveCreatedBy1765557470457';
5+
6+
public async up(query_runner: QueryRunner): Promise<void> {
7+
await query_runner.query(
8+
`ALTER TABLE "hashtag" DROP CONSTRAINT "FK_11c8b3519f62b36dd5385c217d3"`
9+
);
10+
11+
await query_runner.query(`ALTER TABLE "hashtag" DROP COLUMN "created_by"`);
12+
}
13+
14+
public async down(query_runner: QueryRunner): Promise<void> {
15+
await query_runner.query(`ALTER TABLE "hashtag" ADD "created_by" uuid`);
16+
17+
await query_runner.query(
18+
`ALTER TABLE "hashtag" ADD CONSTRAINT "FK_11c8b3519f62b36dd5385c217d3" FOREIGN KEY ("created_by") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
19+
);
20+
}
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
3+
export class TweetHashtagEntity1765585636405 implements MigrationInterface {
4+
name = 'TweetHashtagEntity1765585636405';
5+
6+
public async up(query_runner: QueryRunner): Promise<void> {
7+
await query_runner.query(
8+
`CREATE TABLE "tweet_hashtags" ("tweet_id" uuid NOT NULL, "hashtag_name" character varying NOT NULL, CONSTRAINT "PK_42219b0e52e3bee49d2772b3a54" PRIMARY KEY ("tweet_id", "hashtag_name"))`
9+
);
10+
11+
await query_runner.query(
12+
`ALTER TABLE "tweet_hashtags" ADD CONSTRAINT "FK_efe191c9c3d1359e60bac167736" FOREIGN KEY ("tweet_id") REFERENCES "tweets"("tweet_id") ON DELETE CASCADE ON UPDATE NO ACTION`
13+
);
14+
await query_runner.query(
15+
`ALTER TABLE "tweet_hashtags" ADD CONSTRAINT "FK_b0a40275de4a8088c5e6426419d" FOREIGN KEY ("hashtag_name") REFERENCES "hashtag"("name") ON DELETE CASCADE ON UPDATE NO ACTION`
16+
);
17+
}
18+
19+
public async down(query_runner: QueryRunner): Promise<void> {
20+
await query_runner.query(
21+
`ALTER TABLE "tweet_hashtags" DROP CONSTRAINT "FK_b0a40275de4a8088c5e6426419d"`
22+
);
23+
await query_runner.query(
24+
`ALTER TABLE "tweet_hashtags" DROP CONSTRAINT "FK_efe191c9c3d1359e60bac167736"`
25+
);
26+
27+
await query_runner.query(`DROP TABLE "tweet_hashtags"`);
28+
}
29+
}

src/trend/fake-trend.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ export class FakeTrendService {
2525
) {}
2626

2727
// Every 20 minutes
28-
@Cron('*/20 * * * *', {
29-
name: 'fake-trends-job',
30-
timeZone: 'UTC',
31-
})
28+
// @Cron('*/20 * * * *', {
29+
// name: 'fake-trends-job',
30+
// timeZone: 'UTC',
31+
// })
3232
async fakeTrends(): Promise<void> {
3333
try {
3434
const trend_bot = await this.insertTrendBotIfNotExists();

src/tweets/entities/hashtags.entity.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
Entity,
77
JoinColumn,
88
ManyToOne,
9+
OneToMany,
910
PrimaryColumn,
1011
} from 'typeorm';
12+
import { TweetHashtag } from './tweet-hashtag.entity';
1113

1214
@Entity('hashtag')
1315
export class Hashtag {
@@ -17,14 +19,13 @@ export class Hashtag {
1719
@Column({ type: 'int', default: 0 })
1820
usage_count: number;
1921

20-
@ManyToOne(() => User, (user) => user.hashtags, {})
21-
@JoinColumn({ name: 'created_by', referencedColumnName: 'id' })
22-
created_by: User;
23-
2422
@CreateDateColumn({ type: 'timestamptz' })
2523
created_at: Date;
2624

2725
// I guess we won't need this but just in case
2826
@DeleteDateColumn({ type: 'timestamptz' })
2927
deleted_at: Date;
28+
29+
@OneToMany(() => TweetHashtag, (tweet_hashtag) => tweet_hashtag.hashtag)
30+
tweet_hashtags: TweetHashtag[];
3031
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Entity, ForeignKey, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
2+
import { Tweet } from './tweet.entity';
3+
import { Hashtag } from './hashtags.entity';
4+
5+
@Entity('tweet_hashtags')
6+
export class TweetHashtag {
7+
@PrimaryColumn('uuid')
8+
tweet_id: string;
9+
10+
@PrimaryColumn('varchar')
11+
hashtag_name: string;
12+
13+
@ManyToOne(() => Tweet, (tweet) => tweet.tweet_hashtags, {
14+
onDelete: 'CASCADE',
15+
})
16+
@JoinColumn({ name: 'tweet_id' })
17+
tweet: Tweet;
18+
19+
@ManyToOne(() => Hashtag, (hashtag) => hashtag.tweet_hashtags, {
20+
onDelete: 'CASCADE',
21+
})
22+
@JoinColumn({ name: 'hashtag_name' })
23+
hashtag: Hashtag;
24+
}

0 commit comments

Comments
 (0)