Skip to content

Commit b5bd362

Browse files
authored
feat: eReputation and Dreamsync wishlist synchronization (#455)
* feat: eReputation and Dreamsync wishlist synchronization * chore: remove logs * chore: change eReputation's wishlist preview to render markdown * feat: add user profile data to header
1 parent 60dc4e2 commit b5bd362

17 files changed

Lines changed: 411 additions & 47 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"tableName": "wishlists",
3+
"schemaId": "770e8400-e29b-41d4-a716-446655440000",
4+
"ownerEnamePath": "users(user.ename)",
5+
"ownedJunctionTables": [],
6+
"localToUniversalMap": {
7+
"title": "title",
8+
"content": "content",
9+
"isActive": "isActive",
10+
"isPublic": "isPublic",
11+
"metadata": "metadata",
12+
"user": "users(user.id),userId",
13+
"createdAt": "createdAt",
14+
"updatedAt": "updatedAt"
15+
}
16+
}

platforms/dreamsync-api/src/web3adapter/watchers/subscriber.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ export class PostgresSubscriber implements EntitySubscriberInterface {
7373
}
7474
}
7575

76+
// Special handling for Wishlist entities to ensure user relation is loaded
77+
if (tableName === "wishlists") {
78+
const wishlistRepository = AppDataSource.getRepository("Wishlist");
79+
const fullWishlist = await wishlistRepository.findOne({
80+
where: { id: entity.id },
81+
relations: ["user"]
82+
});
83+
84+
if (fullWishlist && fullWishlist.user) {
85+
enrichedEntity.user = fullWishlist.user;
86+
} else if (entity.userId) {
87+
// Fallback: load user by userId if relation wasn't loaded
88+
const userRepository = AppDataSource.getRepository("User");
89+
const user = await userRepository.findOne({
90+
where: { id: entity.userId }
91+
});
92+
if (user) {
93+
enrichedEntity.user = user;
94+
}
95+
}
96+
}
97+
7698
return this.entityToPlain(enrichedEntity);
7799
} catch (error) {
78100
console.error("Error loading relations:", error);

platforms/eReputation-api/src/controllers/CalculationController.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Request, Response } from "express";
22
import { CalculationService } from "../services/CalculationService";
33
import { authGuard } from "../middleware/auth";
4+
import { AppDataSource } from "../database/data-source";
5+
import { Wishlist } from "../database/entities/Wishlist";
46

57
export class CalculationController {
68
private calculationService: CalculationService;
@@ -25,16 +27,36 @@ export class CalculationController {
2527
finalTargetType = "self";
2628
}
2729

28-
if (!finalTargetType || !finalTargetId || !finalTargetName || !userValues) {
30+
if (!finalTargetType || !finalTargetId || !finalTargetName) {
2931
return res.status(400).json({ error: "Missing required fields" });
3032
}
3133

34+
// If userValues is empty, check if wishlist exists
35+
if (!userValues || !userValues.trim()) {
36+
const wishlistRepository = AppDataSource.getRepository(Wishlist);
37+
const wishlists = await wishlistRepository.find({
38+
where: {
39+
userId: calculatorId,
40+
isActive: true
41+
},
42+
order: { updatedAt: "DESC" },
43+
take: 1
44+
});
45+
46+
if (wishlists.length === 0 || !wishlists[0].content || !wishlists[0].content.trim()) {
47+
return res.status(400).json({
48+
error: "Either provide values you care about, or ensure you have an active wishlist in dreamSync"
49+
});
50+
}
51+
}
52+
3253
// Create calculation record
54+
// If userValues is empty, pass empty string - service will use wishlist
3355
const calculation = await this.calculationService.createCalculation({
3456
targetType: finalTargetType,
3557
targetId: finalTargetId,
3658
targetName: finalTargetName,
37-
userValues,
59+
userValues: userValues || "",
3860
calculatorId
3961
});
4062

platforms/eReputation-api/src/controllers/UserController.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Request, Response } from "express";
22
import { UserService } from "../services/UserService";
3+
import { AppDataSource } from "../database/data-source";
4+
import { Wishlist } from "../database/entities/Wishlist";
35

46
export class UserController {
57
private userService: UserService;
@@ -137,4 +139,42 @@ export class UserController {
137139
res.status(500).json({ error: "Internal server error" });
138140
}
139141
};
142+
143+
getMyWishlist = async (req: Request, res: Response) => {
144+
try {
145+
if (!req.user) {
146+
return res.status(401).json({ error: "Authentication required" });
147+
}
148+
149+
const wishlistRepository = AppDataSource.getRepository(Wishlist);
150+
const wishlists = await wishlistRepository.find({
151+
where: {
152+
userId: req.user.id,
153+
isActive: true
154+
},
155+
order: { updatedAt: "DESC" },
156+
take: 1 // Get the most recent active wishlist
157+
});
158+
159+
if (wishlists.length === 0) {
160+
return res.json({ wishlist: null });
161+
}
162+
163+
const wishlist = wishlists[0];
164+
res.json({
165+
wishlist: {
166+
id: wishlist.id,
167+
title: wishlist.title,
168+
content: wishlist.content,
169+
isActive: wishlist.isActive,
170+
isPublic: wishlist.isPublic,
171+
createdAt: wishlist.createdAt,
172+
updatedAt: wishlist.updatedAt,
173+
}
174+
});
175+
} catch (error) {
176+
console.error("Error getting user wishlist:", error);
177+
res.status(500).json({ error: "Internal server error" });
178+
}
179+
};
140180
}

platforms/eReputation-api/src/controllers/WebhookController.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { User } from "../database/entities/User";
1010
import { Group } from "../database/entities/Group";
1111
import { Poll } from "../database/entities/Poll";
1212
import { VoteReputationResult } from "../database/entities/VoteReputationResult";
13+
import { Wishlist } from "../database/entities/Wishlist";
1314
import { AppDataSource } from "../database/data-source";
1415
import axios from "axios";
1516

@@ -302,6 +303,72 @@ export class WebhookController {
302303
});
303304
}
304305
}
306+
} else if (mapping.tableName === "wishlists") {
307+
const wishlistRepository = AppDataSource.getRepository(Wishlist);
308+
const userRepository = AppDataSource.getRepository(User);
309+
310+
// Get userId from user reference
311+
let userId: string | null = null;
312+
if (local.data.user) {
313+
if (typeof local.data.user === "string" && local.data.user.includes("(")) {
314+
userId = local.data.user.split("(")[1].split(")")[0];
315+
} else if (typeof local.data.user === "object" && local.data.user !== null && "id" in local.data.user) {
316+
userId = (local.data.user as { id: string }).id;
317+
}
318+
} else if (local.data.userId) {
319+
userId = local.data.userId as string;
320+
}
321+
322+
if (!userId) {
323+
return res.status(400).send();
324+
}
325+
326+
// Load user relation
327+
const user = await userRepository.findOne({ where: { id: userId } });
328+
if (!user) {
329+
return res.status(400).send();
330+
}
331+
332+
if (localId) {
333+
// Update existing wishlist
334+
const wishlist = await wishlistRepository.findOne({
335+
where: { id: localId },
336+
relations: ["user"]
337+
});
338+
339+
if (wishlist) {
340+
wishlist.title = local.data.title as string;
341+
wishlist.content = local.data.content as string;
342+
wishlist.isActive = local.data.isActive as boolean ?? true;
343+
wishlist.isPublic = local.data.isPublic as boolean ?? false;
344+
wishlist.metadata = local.data.metadata as any;
345+
wishlist.user = user;
346+
wishlist.userId = userId;
347+
348+
await wishlistRepository.save(wishlist);
349+
finalLocalId = wishlist.id;
350+
}
351+
} else {
352+
// Create new wishlist
353+
const wishlist = wishlistRepository.create({
354+
title: local.data.title as string,
355+
content: local.data.content as string,
356+
isActive: local.data.isActive as boolean ?? true,
357+
isPublic: local.data.isPublic as boolean ?? false,
358+
metadata: local.data.metadata as any,
359+
user: user,
360+
userId: userId
361+
});
362+
363+
const savedWishlist = await wishlistRepository.save(wishlist);
364+
365+
this.adapter.addToLockedIds(savedWishlist.id);
366+
await this.adapter.mappingDb.storeMapping({
367+
localId: savedWishlist.id,
368+
globalId: req.body.id,
369+
});
370+
finalLocalId = savedWishlist.id;
371+
}
305372
}
306373

307374
res.status(200).send();
@@ -315,11 +382,12 @@ export class WebhookController {
315382
if (!poll.groupId) return;
316383

317384
const group = await this.groupService.getGroupById(poll.groupId);
318-
if (!group || !group.charter) return;
385+
if (!group) return;
319386

387+
const charter = (group.charter && group.charter.trim()) ? group.charter : "";
320388
const reputationResults = await this.votingReputationService.calculateGroupMemberReputations(
321389
poll.groupId,
322-
group.charter
390+
charter
323391
);
324392

325393
const voteReputationResult = await this.votingReputationService.saveReputationResults(

platforms/eReputation-api/src/database/data-source.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Vote } from "./entities/Vote";
1010
import { Poll } from "./entities/Poll";
1111
import { VoteReputationResult } from "./entities/VoteReputationResult";
1212
import { Message } from "./entities/Message";
13+
import { Wishlist } from "./entities/Wishlist";
1314
import { PostgresSubscriber } from "../web3adapter/watchers/subscriber";
1415

1516
// Use absolute path for better CLI compatibility
@@ -20,7 +21,7 @@ export const dataSourceOptions: DataSourceOptions = {
2021
type: "postgres",
2122
url: process.env.EREPUTATION_DATABASE_URL,
2223
synchronize: false, // Auto-sync in development
23-
entities: [User, Group, Reference, Calculation, Vote, Poll, VoteReputationResult, Message],
24+
entities: [User, Group, Reference, Calculation, Vote, Poll, VoteReputationResult, Message, Wishlist],
2425
migrations: [path.join(__dirname, "migrations", "*.ts")],
2526
logging: process.env.NODE_ENV === "development",
2627
subscribers: [PostgresSubscriber],

platforms/eReputation-api/src/database/entities/User.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "typeorm";
1111
import { Vote } from "./Vote";
1212
import { Poll } from "./Poll";
13+
import { Wishlist } from "./Wishlist";
1314

1415
@Entity("users")
1516
export class User {
@@ -73,4 +74,7 @@ export class User {
7374

7475
@OneToMany(() => Vote, (vote) => vote.user)
7576
votes!: Vote[];
77+
78+
@OneToMany(() => Wishlist, (wishlist) => wishlist.user)
79+
wishlists!: Wishlist[];
7680
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
UpdateDateColumn,
7+
ManyToOne,
8+
JoinColumn,
9+
} from "typeorm";
10+
import { User } from "./User";
11+
12+
@Entity("wishlists")
13+
export class Wishlist {
14+
@PrimaryGeneratedColumn("uuid")
15+
id!: string;
16+
17+
@Column({ type: "varchar", length: 255 })
18+
title!: string;
19+
20+
@Column({ type: "text" })
21+
content!: string; // Markdown content
22+
23+
@Column({ type: "boolean", default: true })
24+
isActive!: boolean;
25+
26+
@Column({ type: "boolean", default: false })
27+
isPublic!: boolean;
28+
29+
@Column({ type: "jsonb", nullable: true })
30+
metadata?: {
31+
tags?: string[];
32+
categories?: string[];
33+
lastAnalyzed?: Date;
34+
analysisVersion?: number;
35+
};
36+
37+
@ManyToOne(() => User, (user) => user.wishlists, { onDelete: "CASCADE" })
38+
@JoinColumn({ name: "userId" })
39+
user!: User;
40+
41+
@Column({ type: "uuid" })
42+
userId!: string;
43+
44+
@CreateDateColumn()
45+
createdAt!: Date;
46+
47+
@UpdateDateColumn()
48+
updatedAt!: Date;
49+
}
50+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Migration1763660768132 implements MigrationInterface {
4+
name = 'Migration1763660768132'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "wishlists" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(255) NOT NULL, "content" text NOT NULL, "isActive" boolean NOT NULL DEFAULT true, "isPublic" boolean NOT NULL DEFAULT false, "metadata" jsonb, "userId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_d0a37f2848c5d268d315325f359" PRIMARY KEY ("id"))`);
8+
await queryRunner.query(`ALTER TABLE "wishlists" ADD CONSTRAINT "FK_4f3c30555daa6ab0b70a1db772c" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
9+
}
10+
11+
public async down(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query(`ALTER TABLE "wishlists" DROP CONSTRAINT "FK_4f3c30555daa6ab0b70a1db772c"`);
13+
await queryRunner.query(`DROP TABLE "wishlists"`);
14+
}
15+
16+
}

platforms/eReputation-api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ app.get("/api/users/me", authGuard, userController.currentUser);
9191
app.get("/api/users/search", userController.search);
9292
app.get("/api/users/:id", authGuard, userController.getProfileById);
9393
app.patch("/api/users", authGuard, userController.updateProfile);
94+
app.get("/api/users/me/wishlist", authGuard, userController.getMyWishlist);
9495

9596
// Group routes
9697
app.get("/api/groups/search", groupController.search);

0 commit comments

Comments
 (0)