eggi-ai-db-schema
Version:
Type-safe database schema and ORM client for Eggi.AI with direct RDS connection
378 lines • 20 kB
JavaScript
"use strict";
/**
* LinkedIn Profile Caching Utilities
*
* Provides functions to check for existing LinkedIn profiles and retrieve cached data
* to avoid unnecessary API calls when profile data is already available.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCachedLinkedinProfiles = getCachedLinkedinProfiles;
exports.getComprehensiveLinkedinProfile = getComprehensiveLinkedinProfile;
const drizzle_orm_1 = require("drizzle-orm");
const db_1 = require("../lib/db");
const schema_1 = require("../lib/schema");
/**
* Helper function to handle optional string fields
* Returns undefined if value is null, undefined, or empty string
* This ensures optional fields are omitted from JSON response when not present
*/
function optionalString(value) {
if (value === null || value === undefined || value.trim() === "") {
return undefined;
}
return value;
}
/**
* Helper function for required string fields
* Returns empty string if value is null/undefined (required fields must be present)
*/
function requiredString(value) {
return value || "";
}
// checkLinkedinProfileExists removed - not used in consumers
// Use getComprehensiveLinkedinProfile instead for profile existence checks
// getCachedLinkedinProfile removed - only used in test scripts
// Use getComprehensiveLinkedinProfile for production profile retrieval
/**
* Get basic profile information for multiple LinkedIn ACo identifiers
* Useful for conversation partner lookups
*/
async function getCachedLinkedinProfiles(acoIdentifiers) {
try {
const db = await (0, db_1.getDb)();
const profileMap = new Map();
if (acoIdentifiers.length === 0) {
return profileMap;
}
const results = await db
.select({
// Profile data
profileId: schema_1.linkedinProfiles.id,
socialAccountId: schema_1.linkedinProfiles.socialAccountId,
firstName: schema_1.linkedinProfiles.firstName,
lastName: schema_1.linkedinProfiles.lastName,
headline: schema_1.linkedinProfiles.headline,
location: schema_1.linkedinProfiles.location,
profilePictureUrl: schema_1.linkedinProfiles.profilePictureUrl,
followerCount: schema_1.linkedinProfiles.followerCount,
connectionsCount: schema_1.linkedinProfiles.connectionsCount,
profileCreatedAt: schema_1.linkedinProfiles.createdAt,
profileUpdatedAt: schema_1.linkedinProfiles.updatedAt,
// Social account data
internalIdentifier: schema_1.socialAccounts.internalIdentifier,
publicIdentifier: schema_1.socialAccounts.publicIdentifier,
})
.from(schema_1.socialAccounts)
.innerJoin(schema_1.linkedinProfiles, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.id, schema_1.linkedinProfiles.socialAccountId))
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"), (0, drizzle_orm_1.inArray)(schema_1.socialAccounts.internalIdentifier, acoIdentifiers)));
for (const record of results) {
const cachedProfile = {
profileId: record.profileId,
socialAccountId: record.socialAccountId,
internalIdentifier: record.internalIdentifier,
...(record.publicIdentifier && { publicIdentifier: record.publicIdentifier }),
firstName: record.firstName || undefined,
lastName: record.lastName || undefined,
headline: record.headline || undefined,
location: record.location || undefined,
profilePictureUrl: record.profilePictureUrl || undefined,
followerCount: record.followerCount || undefined,
connectionsCount: record.connectionsCount || undefined,
createdAt: record.profileCreatedAt.toISOString(),
updatedAt: record.profileUpdatedAt.toISOString(),
};
if (record.internalIdentifier) {
profileMap.set(record.internalIdentifier, cachedProfile);
}
}
return profileMap;
}
catch (error) {
console.error("Error retrieving cached LinkedIn profiles:", error);
return new Map();
}
}
// getMissingLinkedinProfiles and isProfileStale removed - not used in consumers
/**
* Get comprehensive LinkedIn profile data from database formatted like Unipile response
* This function fetches all related data (education, work experience, skills, etc.)
* and formats it to match the Unipile API response structure as closely as possible
* @param identifier - LinkedIn identifier (supports both ACo IDs and public identifiers)
*/
async function getComprehensiveLinkedinProfile(identifier) {
try {
const db = await (0, db_1.getDb)();
// First get the main profile data
const profileResult = await db
.select({
// Profile data
profileId: schema_1.linkedinProfiles.id,
socialAccountId: schema_1.linkedinProfiles.socialAccountId,
firstName: schema_1.linkedinProfiles.firstName,
lastName: schema_1.linkedinProfiles.lastName,
headline: schema_1.linkedinProfiles.headline,
summary: schema_1.linkedinProfiles.summary,
location: schema_1.linkedinProfiles.location,
profilePictureUrl: schema_1.linkedinProfiles.profilePictureUrl,
followerCount: schema_1.linkedinProfiles.followerCount,
connectionsCount: schema_1.linkedinProfiles.connectionsCount,
// Social account data
internalIdentifier: schema_1.socialAccounts.internalIdentifier,
internalIdentifierRegular: schema_1.socialAccounts.internalIdentifierRegular,
publicIdentifier: schema_1.socialAccounts.publicIdentifier,
})
.from(schema_1.socialAccounts)
.innerJoin(schema_1.linkedinProfiles, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.id, schema_1.linkedinProfiles.socialAccountId))
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"),
// Support all identifier types: ACoA (regular), ACW (internal), and public identifiers
(0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifierRegular, identifier), // ACoA identifiers
(0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifier, identifier), // ACW/AEMA identifiers
(0, drizzle_orm_1.eq)(schema_1.socialAccounts.publicIdentifier, identifier) // Public identifiers
)))
.limit(1);
if (profileResult.length === 0) {
return null;
}
const profile = profileResult[0];
if (!profile) {
return null;
}
const profileId = profile.profileId;
// Fetch all related data in parallel for better performance
const [workExperienceData, educationData, profileSkillsData, certificationsData, projectsData] = await Promise.all([
// Work Experience with skills
db
.select({
id: schema_1.linkedinWorkExperience.id,
position: schema_1.linkedinWorkExperience.position,
company: schema_1.linkedinWorkExperience.company,
companyId: schema_1.linkedinWorkExperience.companyId,
companyLinkedinUrl: schema_1.linkedinWorkExperience.companyLinkedinUrl,
employmentType: schema_1.linkedinWorkExperience.employmentType,
location: schema_1.linkedinWorkExperience.location,
description: schema_1.linkedinWorkExperience.description,
isCurrent: schema_1.linkedinWorkExperience.isCurrent,
startDate: schema_1.linkedinWorkExperience.startDate,
endDate: schema_1.linkedinWorkExperience.endDate,
})
.from(schema_1.linkedinWorkExperience)
.where((0, drizzle_orm_1.eq)(schema_1.linkedinWorkExperience.profileId, profileId))
.orderBy(schema_1.linkedinWorkExperience.startDate),
// Education
db
.select({
id: schema_1.linkedinEducation.id,
degree: schema_1.linkedinEducation.degree,
school: schema_1.linkedinEducation.school,
schoolId: schema_1.linkedinEducation.schoolId,
schoolLinkedinUrl: schema_1.linkedinEducation.schoolLinkedinUrl,
fieldOfStudy: schema_1.linkedinEducation.fieldOfStudy,
grade: schema_1.linkedinEducation.grade,
description: schema_1.linkedinEducation.description,
activitiesAndSocieties: schema_1.linkedinEducation.activitiesAndSocieties,
startDate: schema_1.linkedinEducation.startDate,
endDate: schema_1.linkedinEducation.endDate,
})
.from(schema_1.linkedinEducation)
.where((0, drizzle_orm_1.eq)(schema_1.linkedinEducation.profileId, profileId))
.orderBy(schema_1.linkedinEducation.startDate),
// Profile Skills
db
.select({
skillName: schema_1.skills.name,
endorsementCount: schema_1.linkedinProfileSkills.endorsementCount,
skillOrder: schema_1.linkedinProfileSkills.skillOrder,
})
.from(schema_1.linkedinProfileSkills)
.innerJoin(schema_1.skills, (0, drizzle_orm_1.eq)(schema_1.linkedinProfileSkills.skillId, schema_1.skills.id))
.where((0, drizzle_orm_1.eq)(schema_1.linkedinProfileSkills.profileId, profileId))
.orderBy(schema_1.linkedinProfileSkills.skillOrder),
// Certifications
db
.select({
name: schema_1.linkedinCertifications.name,
organization: schema_1.linkedinCertifications.organization,
url: schema_1.linkedinCertifications.url,
})
.from(schema_1.linkedinCertifications)
.where((0, drizzle_orm_1.eq)(schema_1.linkedinCertifications.profileId, profileId)),
// Projects
db
.select({
name: schema_1.linkedinProjects.name,
description: schema_1.linkedinProjects.description,
startDate: schema_1.linkedinProjects.startDate,
endDate: schema_1.linkedinProjects.endDate,
})
.from(schema_1.linkedinProjects)
.where((0, drizzle_orm_1.eq)(schema_1.linkedinProjects.profileId, profileId)),
]);
// Get skills for each work experience
const workExperienceSkillsData = workExperienceData.length > 0
? await db
.select({
workExperienceId: schema_1.workExperienceSkills.workExperienceId,
skillName: schema_1.skills.name,
})
.from(schema_1.workExperienceSkills)
.innerJoin(schema_1.skills, (0, drizzle_orm_1.eq)(schema_1.workExperienceSkills.skillId, schema_1.skills.id))
.where((0, drizzle_orm_1.inArray)(schema_1.workExperienceSkills.workExperienceId, workExperienceData.map(exp => exp.id)))
: [];
// Group work experience skills by work experience ID
const workExpSkillsMap = new Map();
for (const skillData of workExperienceSkillsData) {
const skills = workExpSkillsMap.get(skillData.workExperienceId) || [];
skills.push(skillData.skillName);
workExpSkillsMap.set(skillData.workExperienceId, skills);
}
// Get skills for each education
const educationSkillsData = educationData.length > 0
? await db
.select({
educationId: schema_1.educationSkills.educationId,
skillName: schema_1.skills.name,
})
.from(schema_1.educationSkills)
.innerJoin(schema_1.skills, (0, drizzle_orm_1.eq)(schema_1.educationSkills.skillId, schema_1.skills.id))
.where((0, drizzle_orm_1.inArray)(schema_1.educationSkills.educationId, educationData.map(edu => edu.id)))
: [];
// Group education skills by education ID
const eduSkillsMap = new Map();
for (const skillData of educationSkillsData) {
const skills = eduSkillsMap.get(skillData.educationId) || [];
skills.push(skillData.skillName);
eduSkillsMap.set(skillData.educationId, skills);
}
// Fetch contact information from contactInfos table for LinkedIn source
const userId = profile.socialAccountId
? (await db
.select({ userId: schema_1.socialAccounts.userId })
.from(schema_1.socialAccounts)
.where((0, drizzle_orm_1.eq)(schema_1.socialAccounts.id, profile.socialAccountId))
.limit(1))[0]?.userId
: null;
const contactInfo = {
emails: [],
phones: [],
adresses: [], // Note: keeping Unipile's typo for compatibility
socials: [],
};
if (userId) {
const contactInfoResults = await db
.select({
type: schema_1.contactInfos.type,
value: schema_1.contactInfos.value,
})
.from(schema_1.contactInfos)
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.contactInfos.userId, userId), (0, drizzle_orm_1.eq)(schema_1.contactInfos.source, "linkedin")));
// Group contact info by type
for (const contact of contactInfoResults) {
switch (contact.type) {
case "email":
contactInfo.emails.push(contact.value);
break;
case "phone_number":
contactInfo.phones.push(contact.value);
break;
// Note: adresses stays empty as we don't store address type in contact_infos
// but we keep the field for Unipile compatibility
}
}
}
// Build the comprehensive response matching Unipile API specification
// Include BOTH identifiers so Lambda can decide which to return
const response = {
object: "UserProfile",
provider: "LINKEDIN",
provider_id: profile.internalIdentifierRegular || profile.internalIdentifier || "", // Simple fallback, Lambda decides final value
// Include BOTH identifiers for Lambda decision logic
internal_identifier_regular: profile.internalIdentifierRegular || undefined,
internal_identifier: profile.internalIdentifier || undefined,
// Required fields per API docs (must always be present)
public_identifier: requiredString(profile.publicIdentifier),
first_name: requiredString(profile.firstName),
last_name: requiredString(profile.lastName),
headline: requiredString(profile.headline),
websites: [], // Required but empty since we removed this field
// Optional fields (omitted when null/empty per API docs)
summary: optionalString(profile.summary),
location: optionalString(profile.location),
profile_picture_url: optionalString(profile.profilePictureUrl),
profile_picture_url_large: optionalString(profile.profilePictureUrl), // Use same as regular
background_picture_url: undefined, // We don't store this
// Network counts
follower_count: profile.followerCount || 0,
connections_count: profile.connectionsCount || 0,
// Contact information (optional)
contact_info: contactInfo.emails.length > 0 || contactInfo.phones.length > 0 ? contactInfo : undefined,
// Work Experience
work_experience: workExperienceData.map(exp => ({
// Required fields per API specification
position: requiredString(exp.position),
company: requiredString(exp.company),
skills: workExpSkillsMap.get(exp.id) || [],
start: requiredString(exp.startDate),
end: requiredString(exp.endDate),
// Optional fields (omitted when null/empty)
company_id: optionalString(exp.companyId),
company_url: optionalString(exp.companyLinkedinUrl),
employment_type: optionalString(exp.employmentType),
location: optionalString(exp.location),
description: optionalString(exp.description),
current: exp.isCurrent || undefined,
})),
// Education (field_of_study is optional per API docs and user request)
education: educationData.map(edu => ({
// Required fields per API specification
school: requiredString(edu.school),
start: requiredString(edu.startDate),
end: requiredString(edu.endDate),
skills: eduSkillsMap.get(edu.id) || [],
// Optional fields (omitted when null/empty, including field_of_study as requested)
degree: optionalString(edu.degree),
school_id: optionalString(edu.schoolId),
school_url: optionalString(edu.schoolLinkedinUrl),
field_of_study: optionalString(edu.fieldOfStudy), // Optional - omitted when null/empty
grade: optionalString(edu.grade),
description: optionalString(edu.description),
activities_and_societies: optionalString(edu.activitiesAndSocieties), // Fixed typo: activities_and_societies
})),
// Skills
skills: profileSkillsData.map(skill => ({
name: skill.skillName,
endorsement_count: skill.endorsementCount || 0,
})),
// Certifications
certifications: certificationsData.map(cert => ({
// Required fields per API specification
name: requiredString(cert.name),
organization: requiredString(cert.organization),
// Optional fields
url: optionalString(cert.url),
})),
// Projects
projects: projectsData.map(project => ({
// Required fields per API specification
name: requiredString(project.name),
description: requiredString(project.description),
start: requiredString(project.startDate),
end: requiredString(project.endDate),
})),
// Fields we don't currently store - only include if required by API spec
languages: [], // Empty array - we don't store language data
volunteering_experience: [], // Empty array - we don't store volunteering data
recommendations: { received: [], given: [] }, // Empty arrays - we don't store recommendations
// NOTE: We do NOT include network/relationship metadata that we don't store:
// - is_relationship, is_self, network_distance, shared_connections_count
// - is_open_profile, is_premium, is_influencer, is_creator
// - is_hiring, is_open_to_work, is_saved_lead, is_crm_imported, can_send_inmail
// These are only available in fresh Unipile responses, not cached data
};
return response;
}
catch (error) {
console.error("Error retrieving comprehensive LinkedIn profile:", error);
return null;
}
}
//# sourceMappingURL=linkedin-profile-cache.js.map