UNPKG

eggi-ai-db-schema

Version:

Type-safe database schema and ORM client for Eggi.AI with direct RDS connection

378 lines 20 kB
"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