UNPKG

eggi-ai-db-schema

Version:

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

624 lines 28.3 kB
"use strict"; /** * ============================================================================= * SOCIAL ACCOUNT OPERATIONS * ============================================================================= * Utilities for managing social media account identifiers and profiles */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.upsertSocialAccount = upsertSocialAccount; exports.getUserSocialAccounts = getUserSocialAccounts; exports.findSocialAccountByLinkedInIdentifier = findSocialAccountByLinkedInIdentifier; exports.findSocialAccountByPlatformIdentifier = findSocialAccountByPlatformIdentifier; exports.getSocialAccountsWithUsers = getSocialAccountsWithUsers; exports.deleteSocialAccountByLinkedInIdentifier = deleteSocialAccountByLinkedInIdentifier; exports.upsertLinkedInAccount = upsertLinkedInAccount; exports.findUserByLinkedInIdentifier = findUserByLinkedInIdentifier; exports.createUser = createUser; exports.createUserWithSocialAccount = createUserWithSocialAccount; exports.getSocialAccountsByCognitoId = getSocialAccountsByCognitoId; exports.upsertLinkedInSocialAccount = upsertLinkedInSocialAccount; const drizzle_orm_1 = require("drizzle-orm"); const db_1 = require("../lib/db"); const schema_1 = require("../lib/schema"); const linkedin_data_operations_1 = require("./linkedin-data-operations"); const linkedin_identifier_utils_1 = require("./linkedin-identifier-utils"); // ============================================================================= // SOCIAL ACCOUNT MANAGEMENT // ============================================================================= /** * Create or update a social account for a user */ async function upsertSocialAccount(data) { const db = await (0, db_1.getDb)(); const socialAccountValues = { userId: data.userId, platform: data.platform, internalIdentifier: data.internalIdentifier || null, internalIdentifierRegular: data.internalIdentifierRegular, // Primary lookup column - always required publicIdentifier: data.publicIdentifier || null, }; (0, db_1.debugLogDbOperation)("upsert", "social_accounts", socialAccountValues, undefined, { operation: "onConflictDoUpdate", userId: data.userId, platform: data.platform, }); const upsertedAccounts = await db .insert(schema_1.socialAccounts) .values(socialAccountValues) .onConflictDoUpdate({ target: [ schema_1.socialAccounts.userId, schema_1.socialAccounts.platform, schema_1.socialAccounts.internalIdentifierRegular, ], set: {}, }) .returning(); (0, db_1.debugLogDbOperation)("upsert", "social_accounts", socialAccountValues, upsertedAccounts, { operation: "completed", userId: data.userId, platform: data.platform, }); return upsertedAccounts[0]; } /** * Get social accounts for a user */ async function getUserSocialAccounts(userId, platform) { const db = await (0, db_1.getDb)(); const whereConditions = [(0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, userId)]; if (platform) { whereConditions.push((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, platform)); } return await db .select() .from(schema_1.socialAccounts) .where((0, drizzle_orm_1.and)(...whereConditions)) .orderBy((0, drizzle_orm_1.asc)(schema_1.socialAccounts.platform), (0, drizzle_orm_1.desc)(schema_1.socialAccounts.createdAt)); } /** * Find social account by LinkedIn identifier with comprehensive complementary identifier search * * COMPREHENSIVE SEARCH: Now searches ALL identifier fields to find complementary identifiers * - ACoA/ACwA/AEMA identifiers -> searches in ALL internal identifier fields * - Public identifiers -> searches in public_identifier field * - URLs -> searches in public_identifier field after extraction * * This enables cache hits when: * - Request: ACwA identifier, Database has: ACoA identifier (complementary pair) * - Request: ACoA identifier, Database has: ACwA identifier (complementary pair) * * STORAGE RULES: * - ACoA identifiers stored in internal_identifier_regular (Sales Navigator routing) * - ACwA identifiers stored in internal_identifier (Regular account routing) * - AEMA identifiers stored in internal_identifier (recruiter accounts) */ async function findSocialAccountByLinkedInIdentifier(identifier) { const db = await (0, db_1.getDb)(); // Determine search strategy based on identifier format let whereClause; let searchType; if (identifier.startsWith("ACoA") || identifier.startsWith("ACwA") || identifier.startsWith("AEMA")) { // For LinkedIn internal identifiers: Search BOTH internal identifier fields // This enables finding complementary identifiers (ACwA request finds ACoA stored data, etc.) whereClause = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"), (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) // ACwA*/AEMA* identifiers )); searchType = "internal_identifier"; } else { // Public identifier (e.g., martin-mohammed) or LinkedIn URL -> search ONLY in publicIdentifier whereClause = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"), (0, drizzle_orm_1.eq)(schema_1.socialAccounts.publicIdentifier, identifier)); searchType = "public_identifier"; } (0, db_1.debugLogDbOperation)("select", "social_accounts", { platform: "linkedin", identifier: identifier, searchType: searchType, }, undefined, { operation: "findSocialAccountByLinkedInIdentifier", identifier_type: searchType, }); const accounts = await db .select({ // Social account fields id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId, platform: schema_1.socialAccounts.platform, internalIdentifier: schema_1.socialAccounts.internalIdentifier, internalIdentifierRegular: schema_1.socialAccounts.internalIdentifierRegular, publicIdentifier: schema_1.socialAccounts.publicIdentifier, createdAt: schema_1.socialAccounts.createdAt, // User fields user: { id: schema_1.users.id, givenName: schema_1.users.givenName, familyName: schema_1.users.familyName, createdAt: schema_1.users.createdAt, }, }) .from(schema_1.socialAccounts) .innerJoin(schema_1.users, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, schema_1.users.id)) .where(whereClause) .limit(1); (0, db_1.debugLogDbOperation)("select", "social_accounts", { platform: "linkedin", identifier: identifier, searchType: searchType, }, accounts, { operation: "findSocialAccountByLinkedInIdentifier", identifier_type: searchType, found: accounts.length > 0, result_id: accounts[0]?.id, }); return accounts[0] || null; } /** * Find social account by platform and identifier (supports both internalIdentifier and publicIdentifier) * @deprecated Use findSocialAccountByLinkedInIdentifier for LinkedIn with proper ACoA/ACw logic */ async function findSocialAccountByPlatformIdentifier(platform, identifier) { // For LinkedIn, use the new specialized function if (platform === "linkedin") { return findSocialAccountByLinkedInIdentifier(identifier); } const db = await (0, db_1.getDb)(); const accounts = await db .select({ // Social account fields id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId, platform: schema_1.socialAccounts.platform, internalIdentifier: schema_1.socialAccounts.internalIdentifier, internalIdentifierRegular: schema_1.socialAccounts.internalIdentifierRegular, publicIdentifier: schema_1.socialAccounts.publicIdentifier, createdAt: schema_1.socialAccounts.createdAt, // User fields user: { id: schema_1.users.id, givenName: schema_1.users.givenName, familyName: schema_1.users.familyName, createdAt: schema_1.users.createdAt, }, }) .from(schema_1.socialAccounts) .innerJoin(schema_1.users, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, schema_1.users.id)) .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, platform), // Check both internalIdentifier (ACo IDs) and publicIdentifier (public URLs) (0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifier, identifier), (0, drizzle_orm_1.eq)(schema_1.socialAccounts.publicIdentifier, identifier)))) .limit(1); return accounts[0] || null; } /** * Get all social accounts with user information */ async function getSocialAccountsWithUsers(platform) { const db = await (0, db_1.getDb)(); const whereConditions = []; if (platform) { whereConditions.push((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, platform)); } return await db .select({ // Social account fields id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId, platform: schema_1.socialAccounts.platform, internalIdentifier: schema_1.socialAccounts.internalIdentifier, internalIdentifierRegular: schema_1.socialAccounts.internalIdentifierRegular, publicIdentifier: schema_1.socialAccounts.publicIdentifier, createdAt: schema_1.socialAccounts.createdAt, // User fields user: { id: schema_1.users.id, givenName: schema_1.users.givenName, familyName: schema_1.users.familyName, createdAt: schema_1.users.createdAt, }, }) .from(schema_1.socialAccounts) .innerJoin(schema_1.users, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, schema_1.users.id)) .where(whereConditions.length > 0 ? (0, drizzle_orm_1.and)(...whereConditions) : undefined) .orderBy((0, drizzle_orm_1.asc)(schema_1.socialAccounts.platform), (0, drizzle_orm_1.desc)(schema_1.socialAccounts.createdAt)); } // ============================================================================= // DELETION OPERATIONS // ============================================================================= /** * Delete social account and all associated data with CASCADE * This will automatically delete: * - LinkedIn profiles (onDelete: "cascade") * - LinkedIn work experience, education, skills, etc. (via CASCADE) * - Unipile accounts (onDelete: "cascade") * - Relationship scores (onDelete: "cascade") * * @param identifier - LinkedIn identifier (ACoA format) * @returns true if account was deleted, false if not found */ async function deleteSocialAccountByLinkedInIdentifier(identifier) { const db = await (0, db_1.getDb)(); // Find the social account first const socialAccount = await db .select({ id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId }) .from(schema_1.socialAccounts) .where((0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifierRegular, identifier)) .limit(1); if (socialAccount.length === 0) { return false; // Account not found } const account = socialAccount[0]; if (!account) { return false; // Safety check } const socialAccountId = account.id; // Delete the social account - CASCADE will handle all related data await db.delete(schema_1.socialAccounts).where((0, drizzle_orm_1.eq)(schema_1.socialAccounts.id, socialAccountId)); return true; // Successfully deleted } // ============================================================================= // DEPRECATED EXPORTS - CLEANED UP // ============================================================================= // getSocialAccountPlatformStats - REMOVED (unused complex stats function) // ============================================================================= // LINKEDIN SPECIFIC OPERATIONS // ============================================================================= /** * Create or update LinkedIn account (ACo identifier) */ async function upsertLinkedInAccount(data) { // Validate that the identifier is in ACoA format if (!data.acoIdentifier.startsWith("ACoA")) { throw new Error(`Invalid ACoA identifier format: ${data.acoIdentifier}. Must start with 'ACoA'`); } const socialAccountData = { userId: data.userId, platform: "linkedin", internalIdentifierRegular: data.acoIdentifier, // Store ACoA identifier in internalIdentifierRegular field }; return await upsertSocialAccount(socialAccountData); } /** * Find user by LinkedIn identifier (supports ACoA, ACw, AEMA, and public identifiers) */ async function findUserByLinkedInIdentifier(identifier) { const db = await (0, db_1.getDb)(); // Use comprehensive search logic for complementary identifiers let whereClause; if (identifier.startsWith("ACoA") || identifier.startsWith("ACwA") || identifier.startsWith("AEMA")) { // For LinkedIn internal identifiers: Search BOTH internal identifier fields // This enables finding complementary identifiers (ACwA request finds ACoA stored data, etc.) whereClause = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"), (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) // ACwA*/AEMA* identifiers )); } else { // Public identifier → search in public_identifier field whereClause = (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, "linkedin"), (0, drizzle_orm_1.eq)(schema_1.socialAccounts.publicIdentifier, identifier)); } const results = await db .select({ user: { id: schema_1.users.id, givenName: schema_1.users.givenName, familyName: schema_1.users.familyName, }, socialAccount: schema_1.socialAccounts, }) .from(schema_1.socialAccounts) .innerJoin(schema_1.users, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, schema_1.users.id)) .where(whereClause) .limit(1); return results[0] || null; } // ============================================================================= // LINKEDIN PROFILE FETCH OPERATIONS // ============================================================================= /** * Create a basic user from LinkedIn profile data */ async function createUser(data) { const db = await (0, db_1.getDb)(); const newUsers = await db .insert(schema_1.users) .values({ givenName: data.givenName || null, familyName: data.familyName || null, }) .returning(); if (newUsers.length === 0) { throw new Error("Failed to create user record"); } return newUsers[0]; } /** * Create user and social account in a single transaction * Used for new LinkedIn profiles that don't exist in the system * * SIMPLIFIED: All LinkedIn identifiers (ACoA, ACw, AEMA) go to internalIdentifier field */ async function createUserWithSocialAccount(data) { const db = await (0, db_1.getDb)(); return await db.transaction(async (tx) => { // Step 0: Check if social account already exists (idempotent) if (data.internalIdentifier || data.publicIdentifier) { // For LinkedIn, use proper identifier lookup logic if (data.platform === "linkedin") { const identifier = data.internalIdentifier || data.publicIdentifier; const existingAccount = await findSocialAccountByLinkedInIdentifier(identifier); if (existingAccount) { return { user: existingAccount.user, socialAccount: { id: existingAccount.id, userId: existingAccount.userId, platform: existingAccount.platform, internalIdentifier: existingAccount.internalIdentifier, internalIdentifierRegular: existingAccount.internalIdentifierRegular, publicIdentifier: existingAccount.publicIdentifier, createdAt: existingAccount.createdAt, }, }; } } else { // For non-LinkedIn platforms, use the old logic const existing = await tx .select({ // Social account fields id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId, platform: schema_1.socialAccounts.platform, internalIdentifier: schema_1.socialAccounts.internalIdentifier, internalIdentifierRegular: schema_1.socialAccounts.internalIdentifierRegular, publicIdentifier: schema_1.socialAccounts.publicIdentifier, createdAt: schema_1.socialAccounts.createdAt, // User fields user: { id: schema_1.users.id, givenName: schema_1.users.givenName, familyName: schema_1.users.familyName, createdAt: schema_1.users.createdAt, }, }) .from(schema_1.socialAccounts) .innerJoin(schema_1.users, (0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, schema_1.users.id)) .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, data.platform), // Check identifier fields based on type (0, drizzle_orm_1.or)(...[ data.internalIdentifier ? (0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifier, data.internalIdentifier) : undefined, data.publicIdentifier ? (0, drizzle_orm_1.eq)(schema_1.socialAccounts.publicIdentifier, data.publicIdentifier) : undefined, ].filter(Boolean)))) .limit(1); if (existing.length > 0) { const record = existing[0]; return { user: record.user, socialAccount: { id: record.id, userId: record.userId, platform: record.platform, internalIdentifier: record.internalIdentifier, internalIdentifierRegular: record.internalIdentifierRegular, publicIdentifier: record.publicIdentifier, createdAt: record.createdAt, }, }; } } } // Step 1: Create the user (only if no existing social account was found) const newUsers = await tx .insert(schema_1.users) .values({ givenName: data.givenName || null, familyName: data.familyName || null, }) .returning(); if (newUsers.length === 0) { throw new Error("Failed to create user record"); } const newUser = newUsers[0]; // Step 2: Create the social account for the new user const socialAccountValues = { userId: newUser.id, platform: data.platform, }; // All LinkedIn identifiers go to internalIdentifier field if (data.internalIdentifier) { socialAccountValues.internalIdentifier = data.internalIdentifier; } if (data.publicIdentifier) { socialAccountValues.publicIdentifier = data.publicIdentifier; } const newSocialAccounts = await tx .insert(schema_1.socialAccounts) .values(socialAccountValues) .returning(); if (newSocialAccounts.length === 0) { throw new Error("Failed to create social account record"); } return { user: newUser, socialAccount: newSocialAccounts[0], }; }); } // ============================================================================= // COGNITO ID BASED LOOKUPS // ============================================================================= /** * Gets social accounts for a user via Cognito ID * * This function: * 1. Gets authenticated user by Cognito ID * 2. Uses authenticated_user.user_id to find social accounts * * @param cognitoUserId - The Cognito User ID * @param platform - Optional platform filter (e.g., "linkedin") * @returns Promise resolving to array of social accounts */ async function getSocialAccountsByCognitoId(cognitoUserId, platform) { const { getAuthenticatedUserByCognitoId } = await Promise.resolve().then(() => __importStar(require("./user-operations"))); // First get the authenticated user const authenticatedUser = await getAuthenticatedUserByCognitoId(cognitoUserId); if (!authenticatedUser) { return []; } // Then get their social accounts using the correct user_id const db = await (0, db_1.getDb)(); const whereConditions = [(0, drizzle_orm_1.eq)(schema_1.socialAccounts.userId, authenticatedUser.userId)]; if (platform) { whereConditions.push((0, drizzle_orm_1.eq)(schema_1.socialAccounts.platform, platform)); } const result = await db .select() .from(schema_1.socialAccounts) .where((0, drizzle_orm_1.and)(...whereConditions)); return result; } /** * Upsert LinkedIn social account with complete profile data * * This function provides a clean, atomic approach to storing LinkedIn profile data: * - Creates user if not exists, reuses if exists * - Creates social account if not exists, reuses if exists * - Updates LinkedIn profile fields with fresh data (preserves additional fields) * - Updates profile.updatedAt timestamp to track when data was refreshed * - Preserves all relationships and additional data not in API response * * @param identifier LinkedIn identifier (ACoA format) * @param profileData Transformed LinkedIn profile data * @param strategy 'fresh' updates profile data, 'cache-first' for new profiles only * @returns Object with user, socialAccount, profileResult, and wasUpdated flag */ async function upsertLinkedInSocialAccount(identifier, profileData, strategy = "cache-first") { // Validate ACoA identifier if it starts with "ACoA" if (identifier.startsWith("ACoA")) { (0, linkedin_identifier_utils_1.validateAcoIdentifier)(identifier); } const db = await (0, db_1.getDb)(); // Step 1: Atomic user and social account upsert const { user, socialAccount, wasUpdated, userWasNewlyCreated, socialAccountWasNewlyCreated } = await db.transaction(async (tx) => { // Upsert user (create if not exists based on profile data) const userFirstName = profileData.first_name || "Unknown"; const userLastName = profileData.last_name || "User"; let user; let userWasNewlyCreated = false; // Try to find existing user by social account const existingSocialAccount = await tx .select({ id: schema_1.socialAccounts.id, userId: schema_1.socialAccounts.userId, }) .from(schema_1.socialAccounts) .where((0, drizzle_orm_1.eq)(schema_1.socialAccounts.internalIdentifierRegular, identifier)) .limit(1); if (existingSocialAccount.length > 0 && existingSocialAccount[0]) { // Use existing user user = { id: existingSocialAccount[0].userId }; userWasNewlyCreated = false; } else { // Create new user const newUsers = await tx .insert(schema_1.users) .values({ givenName: userFirstName, familyName: userLastName, }) .returning({ id: schema_1.users.id }); if (!newUsers[0]) { throw new Error("Failed to create user"); } user = newUsers[0]; userWasNewlyCreated = true; } // Upsert social account (create if not exists) let socialAccount; let socialAccountWasNewlyCreated = false; if (existingSocialAccount.length > 0 && existingSocialAccount[0]) { // Use existing social account socialAccount = { id: existingSocialAccount[0].id }; socialAccountWasNewlyCreated = false; } else { // Create new social account const publicId = profileData.public_identifier || null; const newSocialAccounts = await tx .insert(schema_1.socialAccounts) .values({ userId: user.id, platform: "linkedin", internalIdentifierRegular: identifier, publicIdentifier: publicId, }) .returning({ id: schema_1.socialAccounts.id }); if (!newSocialAccounts[0]) { throw new Error("Failed to create social account"); } socialAccount = newSocialAccounts[0]; socialAccountWasNewlyCreated = true; } return { user, socialAccount, wasUpdated: strategy === "fresh", userWasNewlyCreated, socialAccountWasNewlyCreated, }; }); // Step 2: Store the complete LinkedIn profile (outside transaction due to complexity) const profileResult = await (0, linkedin_data_operations_1.storeCompleteLinkedInProfile)(user.id, socialAccount.id, profileData); return { user, socialAccount, profileResult, wasUpdated, // Creation tracking flags user_was_newly_created: userWasNewlyCreated, social_account_was_newly_created: socialAccountWasNewlyCreated, }; } //# sourceMappingURL=social-account-operations.js.map