Skip to content

Commit 45eab0a

Browse files
authored
Merge pull request #193 from Xaxxoo/engagements
Add social engagement module with DTOs, entities, and controller for …
2 parents 991e5f1 + 4fb3bb4 commit 45eab0a

13 files changed

Lines changed: 1202 additions & 0 deletions
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// src/database/migrations/create-social-engagement-tables.migration.ts
2+
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from 'typeorm';
3+
4+
export class CreateSocialEngagementTables1715076125000 implements MigrationInterface {
5+
public async up(queryRunner: QueryRunner): Promise<void> {
6+
// Create ContentType enum
7+
await queryRunner.query(`
8+
CREATE TYPE "content_type_enum" AS ENUM (
9+
'trading_signal',
10+
'market_analysis',
11+
'community_post',
12+
'comment'
13+
)
14+
`);
15+
16+
// Create EngagementType enum
17+
await queryRunner.query(`
18+
CREATE TYPE "engagement_type_enum" AS ENUM (
19+
'like',
20+
'dislike'
21+
)
22+
`);
23+
24+
// Create social_engagements table
25+
await queryRunner.createTable(
26+
new Table({
27+
name: 'social_engagements',
28+
columns: [
29+
{
30+
name: 'id',
31+
type: 'uuid',
32+
isPrimary: true,
33+
default: 'uuid_generate_v4()',
34+
},
35+
{
36+
name: 'userId',
37+
type: 'uuid',
38+
isNullable: false,
39+
},
40+
{
41+
name: 'contentId',
42+
type: 'uuid',
43+
isNullable: false,
44+
},
45+
{
46+
name: 'contentType',
47+
type: 'content_type_enum',
48+
isNullable: false,
49+
},
50+
{
51+
name: 'type',
52+
type: 'engagement_type_enum',
53+
isNullable: false,
54+
},
55+
{
56+
name: 'metadata',
57+
type: 'jsonb',
58+
isNullable: true,
59+
},
60+
{
61+
name: 'createdAt',
62+
type: 'timestamp',
63+
default: 'CURRENT_TIMESTAMP',
64+
},
65+
{
66+
name: 'updatedAt',
67+
type: 'timestamp',
68+
default: 'CURRENT_TIMESTAMP',
69+
},
70+
],
71+
}),
72+
true,
73+
);
74+
75+
// Create engagement_counters table
76+
await queryRunner.createTable(
77+
new Table({
78+
name: 'engagement_counters',
79+
columns: [
80+
{
81+
name: 'id',
82+
type: 'uuid',
83+
isPrimary: true,
84+
default: 'uuid_generate_v4()',
85+
},
86+
{
87+
name: 'contentId',
88+
type: 'uuid',
89+
isNullable: false,
90+
},
91+
{
92+
name: 'contentType',
93+
type: 'content_type_enum',
94+
isNullable: false,
95+
},
96+
{
97+
name: 'likesCount',
98+
type: 'integer',
99+
default: 0,
100+
},
101+
{
102+
name: 'dislikesCount',
103+
type: 'integer',
104+
default: 0,
105+
},
106+
{
107+
name: 'updatedAt',
108+
type: 'timestamp',
109+
default: 'CURRENT_TIMESTAMP',
110+
},
111+
],
112+
}),
113+
true,
114+
);
115+
116+
// Create indexes
117+
await queryRunner.createIndex(
118+
'social_engagements',
119+
new TableIndex({
120+
name: 'IDX_social_engagements_user_content',
121+
columnNames: ['userId', 'contentId', 'contentType'],
122+
isUnique: true,
123+
}),
124+
);
125+
126+
await queryRunner.createIndex(
127+
'social_engagements',
128+
new TableIndex({
129+
name: 'IDX_social_engagements_user',
130+
columnNames: ['userId'],
131+
}),
132+
);
133+
134+
await queryRunner.createIndex(
135+
'social_engagements',
136+
new TableIndex({
137+
name: 'IDX_social_engagements_content',
138+
columnNames: ['contentId', 'contentType'],
139+
}),
140+
);
141+
142+
await queryRunner.createIndex(
143+
'engagement_counters',
144+
new TableIndex({
145+
name: 'IDX_engagement_counters_content',
146+
columnNames: ['contentId', 'contentType'],
147+
isUnique: true,
148+
}),
149+
);
150+
151+
// Add foreign key for userId if users table exists
152+
const usersTableExists = await queryRunner.hasTable('users');
153+
if (usersTableExists) {
154+
await queryRunner.createForeignKey(
155+
'social_engagements',
156+
new TableForeignKey({
157+
columnNames: ['userId'],
158+
referencedColumnNames: ['id'],
159+
referencedTableName: 'users',
160+
onDelete: 'CASCADE',
161+
}),
162+
);
163+
}
164+
}
165+
166+
public async down(queryRunner: QueryRunner): Promise<void> {
167+
// Drop foreign keys first
168+
const usersTableExists = await queryRunner.hasTable('users');
169+
if (usersTableExists) {
170+
const table = await queryRunner.getTable('social_engagements');
171+
const foreignKey = table.foreignKeys.find(
172+
(fk) => fk.columnNames.indexOf('userId') !== -1,
173+
);
174+
if (foreignKey) {
175+
await queryRunner.dropForeignKey('social_engagements', foreignKey);
176+
}
177+
}
178+
179+
// Drop tables
180+
await queryRunner.dropTable('engagement_counters');
181+
await queryRunner.dropTable('social_engagements');
182+
183+
// Drop enums
184+
await queryRunner.query(`DROP TYPE "engagement_type_enum"`);
185+
await queryRunner.query(`DROP TYPE "content_type_enum"`);
186+
}
187+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// src/shared/enums/content-type.enum.ts
2+
export enum ContentType {
3+
TRADING_SIGNAL = 'trading_signal',
4+
MARKET_ANALYSIS = 'market_analysis',
5+
COMMUNITY_POST = 'community_post',
6+
COMMENT = 'comment',
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// src/shared/enums/engagement-type.enum.ts
2+
export enum EngagementType {
3+
LIKE = 'like',
4+
DISLIKE = 'dislike',
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// src/shared/interfaces/content-polymorph.interface.ts
2+
import { ContentType } from '../enums/content-type.enum';
3+
4+
export interface ContentPolymorph {
5+
contentId: string;
6+
contentType: ContentType;
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// src/social-engagement/dto/create-engagement.dto.ts
2+
import { IsEnum, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
3+
import { EngagementType } from '../../shared/enums/engagement-type.enum';
4+
import { ContentType } from '../../shared/enums/content-type.enum';
5+
6+
export class CreateEngagementDto {
7+
@IsNotEmpty()
8+
@IsUUID()
9+
contentId: string;
10+
11+
@IsNotEmpty()
12+
@IsEnum(ContentType)
13+
contentType: ContentType;
14+
15+
@IsNotEmpty()
16+
@IsEnum(EngagementType)
17+
type: EngagementType;
18+
19+
@IsOptional()
20+
metadata?: Record<string, any>;
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// src/social-engagement/dto/engagement-response.dto.ts
2+
import { EngagementType } from '../../shared/enums/engagement-type.enum';
3+
import { ContentType } from '../../shared/enums/content-type.enum';
4+
5+
export class EngagementResponseDto {
6+
id: string;
7+
userId: string;
8+
contentId: string;
9+
contentType: ContentType;
10+
type: EngagementType;
11+
createdAt: Date;
12+
updatedAt: Date;
13+
counters: {
14+
likes: number;
15+
dislikes: number;
16+
};
17+
metadata?: Record<string, any>;
18+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// src/social-engagement/entities/engagement-counter.entity.ts
2+
import { Entity, Column, PrimaryGeneratedColumn, Index, UpdateDateColumn } from 'typeorm';
3+
import { ContentType } from '../../shared/enums/content-type.enum';
4+
5+
@Entity('engagement_counters')
6+
@Index(['contentId', 'contentType'], { unique: true })
7+
export class EngagementCounter {
8+
@PrimaryGeneratedColumn('uuid')
9+
id: string;
10+
11+
@Column()
12+
@Index()
13+
contentId: string;
14+
15+
@Column({
16+
type: 'enum',
17+
enum: ContentType,
18+
})
19+
contentType: ContentType;
20+
21+
@Column({ default: 0 })
22+
likesCount: number;
23+
24+
@Column({ default: 0 })
25+
dislikesCount: number;
26+
27+
@UpdateDateColumn()
28+
updatedAt: Date;
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// src/social-engagement/entities/social-engagement.entity.ts
2+
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
3+
import { User } from '../../users/entities/user.entity';
4+
import { EngagementType } from '../../shared/enums/engagement-type.enum';
5+
import { ContentType } from '../../shared/enums/content-type.enum';
6+
7+
@Entity('social_engagements')
8+
@Index(['userId', 'contentId', 'contentType'], { unique: true })
9+
export class SocialEngagement {
10+
@PrimaryGeneratedColumn('uuid')
11+
id: string;
12+
13+
@Column()
14+
@Index()
15+
userId: string;
16+
17+
@ManyToOne(() => User)
18+
@JoinColumn({ name: 'userId' })
19+
user: User;
20+
21+
@Column()
22+
@Index()
23+
contentId: string;
24+
25+
@Column({
26+
type: 'enum',
27+
enum: ContentType,
28+
})
29+
contentType: ContentType;
30+
31+
@Column({
32+
type: 'enum',
33+
enum: EngagementType,
34+
})
35+
type: EngagementType;
36+
37+
@CreateDateColumn()
38+
createdAt: Date;
39+
40+
@UpdateDateColumn()
41+
updatedAt: Date;
42+
43+
// Additional metadata can be stored as JSON
44+
@Column({ type: 'jsonb', nullable: true })
45+
metadata: Record<string, any>;
46+
}

0 commit comments

Comments
 (0)