eggi-ai-db-schema
Version:
Type-safe database schema and ORM client for Eggi.AI with direct RDS connection
624 lines • 28.3 kB
JavaScript
"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