Skip to content

Commit 24c7c64

Browse files
committed
fix: dreamsync profile visibility
1 parent 58132d9 commit 24c7c64

12 files changed

Lines changed: 203 additions & 39 deletions

File tree

platforms/dreamsync/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "tsc",
1010
"test-matchmaking": "ts-node test-matchmaking-mock.ts",
1111
"typeorm": "typeorm-ts-node-commonjs",
12-
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts",
12+
"migration:generate": "bash -c 'read -p \"Migration name: \" name && npx typeorm-ts-node-commonjs migration:generate src/database/migrations/$name -d src/database/data-source.ts'",
1313
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts",
1414
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/database/data-source.ts",
1515
"migrate-summaries": "ts-node --project tsconfig.json scripts/migrate-summaries.ts"

platforms/dreamsync/api/src/controllers/ProfessionalProfileController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class ProfessionalProfileController {
7070
"skills",
7171
"workExperience",
7272
"education",
73-
"isDreamsyncVisible",
73+
"isPublic",
7474
];
7575

7676
const updateData: Record<string, unknown> = {};

platforms/dreamsync/api/src/controllers/WebhookController.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GroupService } from "../services/GroupService";
44
import { MessageService } from "../services/MessageService";
55
import { ConsentService } from "../services/ConsentService";
66
import { WebhookProcessingService } from "../services/WebhookProcessingService";
7+
import { ProfessionalProfileService } from "../services/ProfessionalProfileService";
78
import { adapter } from "../web3adapter/watchers/subscriber";
89
import { User } from "../database/entities/User";
910
import { Group } from "../database/entities/Group";
@@ -16,6 +17,7 @@ export class WebhookController {
1617
messageService: MessageService;
1718
consentService: ConsentService;
1819
webhookProcessingService: WebhookProcessingService;
20+
professionalProfileService: ProfessionalProfileService;
1921
adapter: typeof adapter;
2022

2123
constructor() {
@@ -24,6 +26,7 @@ export class WebhookController {
2426
this.messageService = new MessageService();
2527
this.consentService = new ConsentService();
2628
this.webhookProcessingService = new WebhookProcessingService();
29+
this.professionalProfileService = new ProfessionalProfileService();
2730
this.adapter = adapter;
2831
}
2932

@@ -352,10 +355,16 @@ export class WebhookController {
352355
}
353356
}
354357
} else if (mapping.tableName === "professional_profiles") {
355-
// Professional profiles are stored in evault only - DreamSync fetches on demand.
356-
// No local storage; just acknowledge the webhook to prevent retries.
357-
console.log("Professional profile webhook - no local storage (fetched from evault on demand)");
358-
finalLocalId = null;
358+
// Maintain local copy for matching (avoids per-user evault calls during matching)
359+
const ename = req.body.w3id;
360+
const data = req.body.data ?? {};
361+
const prof = await this.professionalProfileService.upsertFromWebhook(ename, data);
362+
if (prof) {
363+
console.log("Professional profile webhook - upserted for ename:", prof.ename);
364+
finalLocalId = prof.id;
365+
} else {
366+
finalLocalId = null;
367+
}
359368
}
360369

361370
// Mark webhook as completed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Wishlist } from "./entities/Wishlist";
99
import { Match } from "./entities/Match";
1010
import { UserEVaultMapping } from "./entities/UserEVaultMapping";
1111
import { WebhookProcessing } from "./entities/WebhookProcessing";
12+
import { ProfessionalProfile } from "./entities/ProfessionalProfile";
1213
import { PostgresSubscriber } from "../web3adapter/watchers/subscriber";
1314

1415
// Use absolute path for better CLI compatibility
@@ -20,7 +21,7 @@ export const dataSourceOptions: DataSourceOptions = {
2021
type: "postgres",
2122
url: process.env.DREAMSYNC_DATABASE_URL,
2223
synchronize: false, // Auto-sync in development
23-
entities: [User, Group, Message, Wishlist, Match, UserEVaultMapping, WebhookProcessing],
24+
entities: [User, Group, Message, Wishlist, Match, UserEVaultMapping, WebhookProcessing, ProfessionalProfile],
2425
migrations: [path.join(__dirname, "migrations", "*.ts")],
2526
logging: process.env.NODE_ENV === "development",
2627
subscribers: [PostgresSubscriber],
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
UpdateDateColumn,
7+
} from "typeorm";
8+
9+
@Entity("professional_profiles")
10+
export class ProfessionalProfile {
11+
@PrimaryGeneratedColumn("uuid")
12+
id!: string;
13+
14+
@Column({ unique: true })
15+
ename!: string;
16+
17+
@Column({ nullable: true })
18+
displayName!: string;
19+
20+
@Column({ nullable: true })
21+
headline!: string;
22+
23+
@Column({ type: "text", nullable: true })
24+
bio!: string;
25+
26+
@Column({ nullable: true })
27+
avatarFileId!: string;
28+
29+
@Column({ nullable: true })
30+
bannerFileId!: string;
31+
32+
@Column({ nullable: true })
33+
cvFileId!: string;
34+
35+
@Column({ nullable: true })
36+
videoIntroFileId!: string;
37+
38+
@Column({ nullable: true })
39+
location!: string;
40+
41+
@Column("text", { array: true, nullable: true })
42+
skills!: string[];
43+
44+
@Column("jsonb", { nullable: true })
45+
workExperience!: object[];
46+
47+
@Column("jsonb", { nullable: true })
48+
education!: object[];
49+
50+
@Column({ default: true })
51+
isPublic!: boolean;
52+
53+
@Column("jsonb", { nullable: true })
54+
socialLinks!: object[];
55+
56+
@CreateDateColumn()
57+
createdAt!: Date;
58+
59+
@UpdateDateColumn()
60+
updatedAt!: Date;
61+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class ProfessionalProfiles1773665365495 implements MigrationInterface {
4+
name = 'ProfessionalProfiles1773665365495'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "professional_profiles" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ename" character varying NOT NULL, "displayName" character varying, "headline" character varying, "bio" text, "avatarFileId" character varying, "bannerFileId" character varying, "cvFileId" character varying, "videoIntroFileId" character varying, "location" character varying, "skills" text array, "workExperience" jsonb, "education" jsonb, "isPublic" boolean NOT NULL DEFAULT true, "socialLinks" jsonb, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_bfb533ecd1e10f08359f04cf0e2" UNIQUE ("ename"), CONSTRAINT "PK_b2140d2f56b0910e4c58ab4d2a2" PRIMARY KEY ("id"))`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`DROP TABLE "professional_profiles"`);
12+
}
13+
14+
}

platforms/dreamsync/api/src/services/AIMatchingService.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { MatchingService, MatchResult, WishlistData, GroupData } from "./Matchin
88
import { withOperationContext } from "../context/OperationContext";
99
import OpenAI from "openai";
1010
import { WishlistSummaryService } from "./WishlistSummaryService";
11-
import { RegistryService } from "./RegistryService";
12-
import { EVaultProfileService } from "./EVaultProfileService";
11+
import { ProfessionalProfileService } from "./ProfessionalProfileService";
1312
import type { ProfessionalProfile } from "../types/profile";
1413

1514
export class AIMatchingService {
@@ -20,8 +19,7 @@ export class AIMatchingService {
2019
private notificationService: MatchNotificationService;
2120
private openai: OpenAI;
2221
private wishlistSummaryService: WishlistSummaryService;
23-
24-
private evaultProfileService: EVaultProfileService;
22+
private professionalProfileService: ProfessionalProfileService;
2523

2624
constructor() {
2725
this.matchingService = new MatchingService();
@@ -33,7 +31,7 @@ export class AIMatchingService {
3331
apiKey: process.env.OPENAI_API_KEY,
3432
});
3533
this.wishlistSummaryService = WishlistSummaryService.getInstance();
36-
this.evaultProfileService = new EVaultProfileService(new RegistryService());
34+
this.professionalProfileService = new ProfessionalProfileService();
3735
}
3836

3937
async findMatches(): Promise<void> {
@@ -57,24 +55,9 @@ export class AIMatchingService {
5755
const existingGroups = await this.getExistingGroups();
5856
console.log(`🏠 Found ${existingGroups.length} existing groups to consider`);
5957

60-
// Fetch professional profiles from eVault (batch, with caching)
61-
const profileCache = new Map<string, ProfessionalProfile | null>();
58+
// Load professional profiles from local DB (populated via webhooks)
6259
const uniqueEnames = [...new Set(wishlists.map((w) => w.user.ename).filter(Boolean))];
63-
await Promise.all(
64-
uniqueEnames.map(async (ename) => {
65-
try {
66-
const prof = await this.evaultProfileService.getProfessionalProfile(ename);
67-
if (prof.isDreamsyncVisible !== false) {
68-
profileCache.set(ename, prof);
69-
} else {
70-
profileCache.set(ename, null);
71-
}
72-
} catch (err) {
73-
console.error(`Failed to fetch professional profile for ${ename}:`, err);
74-
profileCache.set(ename, null);
75-
}
76-
}),
77-
);
60+
const profileCache = await this.professionalProfileService.getByEnames(uniqueEnames);
7861

7962
// Convert to shared service format, filtering out wishlists without summaries
8063
const wishlistData: WishlistData[] = wishlists

platforms/dreamsync/api/src/services/EVaultProfileService.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ export class EVaultProfileService {
162162
website: profData.website,
163163
location: profData.location,
164164
isPublic: profData.isPublic ?? true,
165-
isDreamsyncVisible: profData.isDreamsyncVisible ?? true,
166165
workExperience: profData.workExperience ?? [],
167166
education: profData.education ?? [],
168167
skills: profData.skills ?? [],
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { In, Repository } from "typeorm";
2+
import { AppDataSource } from "../database/data-source";
3+
import { ProfessionalProfile as ProfessionalProfileEntity } from "../database/entities/ProfessionalProfile";
4+
import type { ProfessionalProfile } from "../types/profile";
5+
6+
function normalizeEname(w3id: string | undefined): string | null {
7+
if (!w3id || typeof w3id !== "string") return null;
8+
return w3id.startsWith("@") ? w3id.slice(1) : w3id;
9+
}
10+
11+
export class ProfessionalProfileService {
12+
private repository: Repository<ProfessionalProfileEntity>;
13+
14+
constructor() {
15+
this.repository = AppDataSource.getRepository(ProfessionalProfileEntity);
16+
}
17+
18+
/**
19+
* Upsert professional profile from webhook payload.
20+
* Data should have universal/evault field names (displayName, headline, etc.)
21+
*/
22+
async upsertFromWebhook(w3id: string | undefined, data: Record<string, unknown>): Promise<ProfessionalProfileEntity | null> {
23+
const ename = normalizeEname(w3id);
24+
if (!ename) return null;
25+
26+
let existing = await this.repository.findOne({ where: { ename } });
27+
28+
const payload: Partial<ProfessionalProfileEntity> = {
29+
displayName: (data.displayName ?? data.name) as string | undefined,
30+
headline: data.headline as string | undefined,
31+
bio: data.bio as string | undefined,
32+
avatarFileId: data.avatarFileId as string | undefined,
33+
bannerFileId: data.bannerFileId as string | undefined,
34+
cvFileId: data.cvFileId as string | undefined,
35+
videoIntroFileId: data.videoIntroFileId as string | undefined,
36+
location: data.location as string | undefined,
37+
skills: Array.isArray(data.skills) ? (data.skills as string[]) : undefined,
38+
workExperience: Array.isArray(data.workExperience) ? (data.workExperience as object[]) : undefined,
39+
education: Array.isArray(data.education) ? (data.education as object[]) : undefined,
40+
socialLinks: Array.isArray(data.socialLinks) ? (data.socialLinks as object[]) : undefined,
41+
isPublic: data.isPublic === true,
42+
};
43+
44+
if (existing) {
45+
Object.assign(existing, payload);
46+
return this.repository.save(existing);
47+
}
48+
49+
const created = this.repository.create({
50+
ename,
51+
...payload,
52+
} as Partial<ProfessionalProfileEntity>);
53+
return this.repository.save(created);
54+
}
55+
56+
/**
57+
* Load professional profiles for multiple enames. Returns a Map for quick lookup.
58+
*/
59+
async getByEnames(enames: string[]): Promise<Map<string, ProfessionalProfile>> {
60+
const normalized = enames.map((e) => (e.startsWith("@") ? e.slice(1) : e));
61+
const unique = [...new Set(normalized)].filter(Boolean);
62+
if (unique.length === 0) return new Map();
63+
64+
const rows = await this.repository.find({
65+
where: { ename: In(unique) },
66+
});
67+
68+
const map = new Map<string, ProfessionalProfile>();
69+
for (const row of rows) {
70+
if (row.isPublic) {
71+
const profile = this.entityToProfile(row);
72+
map.set(row.ename, profile);
73+
map.set(`@${row.ename}`, profile); // support lookup with or without @ prefix
74+
}
75+
}
76+
return map;
77+
}
78+
79+
private entityToProfile(entity: ProfessionalProfileEntity): ProfessionalProfile {
80+
const workExp = entity.workExperience as ProfessionalProfile["workExperience"];
81+
const edu = entity.education as ProfessionalProfile["education"];
82+
return {
83+
displayName: entity.displayName ?? undefined,
84+
headline: entity.headline ?? undefined,
85+
bio: entity.bio ?? undefined,
86+
avatarFileId: entity.avatarFileId ?? undefined,
87+
bannerFileId: entity.bannerFileId ?? undefined,
88+
cvFileId: entity.cvFileId ?? undefined,
89+
videoIntroFileId: entity.videoIntroFileId ?? undefined,
90+
location: entity.location ?? undefined,
91+
skills: entity.skills ?? undefined,
92+
workExperience: workExp ?? undefined,
93+
education: edu ?? undefined,
94+
isPublic: entity.isPublic,
95+
} as ProfessionalProfile;
96+
}
97+
}

platforms/dreamsync/api/src/types/profile.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export interface ProfessionalProfile {
4040
website?: string;
4141
location?: string;
4242
isPublic?: boolean;
43-
isDreamsyncVisible?: boolean;
4443
workExperience?: WorkExperience[];
4544
education?: Education[];
4645
skills?: string[];

0 commit comments

Comments
 (0)