UNPKG

eggi-ai-db-schema

Version:

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

1,065 lines (959 loc) 41.9 kB
/** * ============================================================================= * DRIZZLE SCHEMA FOR EGGI.AI AUTHENTICATION & SOCIAL ACCOUNTS * ============================================================================= * This schema automatically generates TypeScript types and provides a type-safe query builder */ import { pgTable, serial, varchar, timestamp, boolean, integer, jsonb, pgEnum, index, unique, text, pgSchema, check, } from "drizzle-orm/pg-core"; import { sql, relations } from "drizzle-orm"; // ============================================================================= // SCHEMAS // ============================================================================= export const linkedinSchema = pgSchema("linkedin"); export const monitoringSchema = pgSchema("monitoring"); // ============================================================================= // ENUMS // ============================================================================= export const socialPlatformTypeEnum = pgEnum("social_platform_type", [ "linkedin", "whatsapp", "twitter", "instagram", "facebook", ]); // messageTypeEnum removed (moved to long-term storage database) export const contactInfoTypeEnum = pgEnum("contact_info_type", ["email", "phone_number", "url"]); // Enum for contact info sources export const contactInfoSourceEnum = pgEnum("contact_info_source", [ "signup", "linkedin", "manual", "import", ]); export const socialAccountTypeEnum = pgEnum("social_account_type", [ "linkedin", "whatsapp", "instagram", "twitter", "facebook", "tiktok", "youtube", "github", "other", ]); export const unipileAccountTypeEnum = pgEnum("unipile_account_type", [ "linkedin_regular", "linkedin_sales_navigator", ]); // LinkedIn specific enums export const linkedinInvitationTypeEnum = pgEnum("linkedin_invitation_type", [ "SENT", "RECEIVED", "NONE", ]); export const linkedinInvitationStatusEnum = pgEnum("linkedin_invitation_status", [ "PENDING", "ACCEPTED", "DECLINED", "WITHDRAWN", ]); export const linkedinNetworkDistanceEnum = pgEnum("linkedin_network_distance", [ "FIRST_DEGREE", "SECOND_DEGREE", "THIRD_DEGREE", "OUT_OF_NETWORK", ]); export const proficiencyLevelEnum = pgEnum("proficiency_level", [ "ELEMENTARY", "LIMITED_WORKING", "PROFESSIONAL_WORKING", "FULL_PROFESSIONAL", "NATIVE_OR_BILINGUAL", ]); export const analysisTypeEnum = pgEnum("analysis_type", ["MESSAGE_ANALYSER", "FEED_ANALYSER"]); export const mappingTypeEnum = pgEnum("mapping_type", [ "linkedin_conversations", // Account worker - processes conversations and messages "linkedin_feed", // Mapping service - processes feeds and profiles ]); // ============================================================================= // TABLES // ============================================================================= // Base users table - represents actual people export const users = pgTable( "users", { id: serial("id").primaryKey(), givenName: varchar("given_name", { length: 255 }), familyName: varchar("family_name", { length: 255 }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ givenNameIndex: index("idx_users_given_name").on(table.givenName), familyNameIndex: index("idx_users_family_name").on(table.familyName), }) ); // Contact information table - stores various contact details for users export const contactInfos = pgTable( "contact_infos", { id: serial("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), type: contactInfoTypeEnum("type").notNull(), value: varchar("value", { length: 255 }).notNull(), source: contactInfoSourceEnum("source").notNull().default("manual"), metadata: jsonb("metadata"), // Additional data (e.g., confidence, verification) createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ userIdIndex: index("idx_contact_infos_user_id").on(table.userId), typeIndex: index("idx_contact_infos_type").on(table.type), // Unique constraint: same email from same source should be unique across all users uniqueTypeValueSource: unique("contact_infos_type_value_source_key").on( table.type, table.value, table.source ), valueIndex: index("idx_contact_infos_value").on(table.value), sourceIndex: index("idx_contact_infos_source").on(table.source), typeValueIndex: index("idx_contact_infos_type_value").on(table.type, table.value), }) ); // Social accounts table - stores social media account identifiers for users export const socialAccounts = pgTable( "social_accounts", { id: serial("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), platform: socialAccountTypeEnum("platform").notNull(), internalIdentifier: varchar("internal_identifier", { length: 255 }), // Internal identifier (was linkedin_identifier) - nullable for ACoA identifiers internalIdentifierRegular: varchar("internal_identifier_regular", { length: 39 }).notNull(), // Primary lookup column - ACoA identifier (exactly 39 chars) - mandatory for all social accounts publicIdentifier: varchar("public_identifier", { length: 255 }), // Optional public identifier (e.g., martin-mohammed) // Removed: unipile_account_id - relationship is now via unipile_accounts.social_account_id createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ userIdIndex: index("idx_social_accounts_user_id").on(table.userId), platformIndex: index("idx_social_accounts_platform").on(table.platform), internalIdentifierIndex: index("idx_social_accounts_internal_identifier").on( table.internalIdentifier ), platformInternalIdentifierIndex: index("idx_social_accounts_platform_internal_identifier").on( table.platform, table.internalIdentifier ), // Unique constraint: one account per user per platform per internal identifier userPlatformInternalIdentifierUnique: unique( "social_accounts_user_platform_internal_identifier_key" ).on(table.userId, table.platform, table.internalIdentifier), // Unique constraint: each internal identifier should be unique across the platform platformInternalIdentifierUnique: unique( "social_accounts_platform_internal_identifier_unique" ).on(table.platform, table.internalIdentifier), // Unique constraint: each internal_identifier_regular should be unique globally // This prevents duplicate ACoA identifiers across all social accounts internalIdentifierRegularUnique: unique( "social_accounts_internal_identifier_regular_unique" ).on(table.internalIdentifierRegular), // CRITICAL: Regex constraints to enforce identifier routing // internal_identifier must start with "ACwA" (for Unipile/authenticated identifiers) internalIdentifierFormatCheck: check( "chk_internal_identifier_format", sql`internal_identifier IS NULL OR internal_identifier ~ '^ACwA'` ), // internal_identifier_regular must start with "ACoA" (primary lookup column) internalIdentifierRegularFormatCheck: check( "chk_internal_identifier_regular_format", sql`internal_identifier_regular ~ '^ACoA'` ), // internal_identifier_regular must be exactly 39 characters internalIdentifierRegularLengthCheck: check( "chk_internal_identifier_regular_length", sql`LENGTH(internal_identifier_regular) = 39` ), }) ); // Authentication linking table - links users to authentication providers export const authenticatedUsers = pgTable( "authenticated_users", { id: serial("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), cognitoUserId: varchar("cognito_user_id", { length: 255 }).unique().notNull(), authProvider: varchar("auth_provider", { length: 50 }).default("cognito").notNull(), // cognito, google, etc. createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ userIdIndex: index("idx_authenticated_users_user_id").on(table.userId), cognitoUserIdIndex: index("idx_authenticated_users_cognito_id").on(table.cognitoUserId), authProviderIndex: index("idx_authenticated_users_auth_provider").on(table.authProvider), }) ); export const unipileAccounts = pgTable( "unipile_accounts", { id: serial("id").primaryKey(), socialAccountId: integer("social_account_id") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), unipileAccountId: varchar("unipile_account_id", { length: 255 }).unique().notNull(), accountType: unipileAccountTypeEnum("account_type").default("linkedin_regular").notNull(), // Account details (name, profile) stored in social_accounts table isActive: boolean("is_active").default(true).notNull(), connectedAt: timestamp("connected_at", { withTimezone: true }).defaultNow().notNull(), lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }), metadata: jsonb("metadata"), // Unipile-specific data createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ socialAccountIdIndex: index("idx_unipile_accounts_social_account_id").on(table.socialAccountId), unipileAccountIdIndex: index("idx_unipile_accounts_unipile_id").on(table.unipileAccountId), isActiveIndex: index("idx_unipile_accounts_active").on(table.isActive), // One Unipile account per social account socialAccountUnique: unique("unipile_accounts_social_account_unique").on(table.socialAccountId), }) ); // ============================================================================= // LINKEDIN PROFILE TABLES // ============================================================================= // Main LinkedIn profiles table export const linkedinProfiles = linkedinSchema.table( "profiles", { id: serial("id").primaryKey(), socialAccountId: integer("social_account_id") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), // Basic profile information // Note: linkedinId removed - use socialAccount.internalIdentifier instead firstName: varchar("first_name", { length: 255 }), lastName: varchar("last_name", { length: 255 }), headline: text("headline"), summary: text("summary"), location: varchar("location", { length: 500 }), // Profile images profilePictureUrl: text("profile_picture_url"), // Network information followerCount: integer("follower_count"), connectionsCount: integer("connections_count"), // Metadata createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ socialAccountIdIndex: index("idx_profiles_social_account").on(table.socialAccountId), // Note: linkedinIdIndex removed - use socialAccount.internalIdentifier for LinkedIn ID // Unique constraint: one profile per social account socialAccountUnique: unique("profiles_social_account_unique").on(table.socialAccountId), // Note: linkedinIdUnique removed - use socialAccount.internalIdentifier for LinkedIn ID }) ); // Note: LinkedIn contact info now stored in main contact_infos table with source="linkedin" // LinkedIn work experience export const linkedinWorkExperience = linkedinSchema.table( "work_experience", { id: serial("id").primaryKey(), profileId: integer("profile_id") .notNull() .references(() => linkedinProfiles.id, { onDelete: "cascade" }), position: varchar("position", { length: 500 }), company: varchar("company", { length: 500 }), companyId: varchar("company_id", { length: 255 }), // LinkedIn company ID companyLinkedinUrl: text("company_linkedin_url"), // LinkedIn URL of the company location: varchar("location", { length: 500 }), employmentType: varchar("employment_type", { length: 255 }), // e.g., "Full-time", "Part-time", "Contract", etc. description: text("description"), // Note: skills moved to work_experience_skills junction table isCurrent: boolean("is_current").default(false), startDate: varchar("start_date", { length: 50 }), // e.g., "2023-01" or "2023" endDate: varchar("end_date", { length: 50 }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ profileIdIndex: index("idx_work_experience_profile_id").on(table.profileId), companyIndex: index("idx_work_experience_company").on(table.company), currentIndex: index("idx_work_experience_current").on(table.isCurrent), profileCurrentIndex: index("idx_work_experience_profile_current").on( table.profileId, table.isCurrent ), }) ); // LinkedIn education export const linkedinEducation = linkedinSchema.table( "education", { id: serial("id").primaryKey(), profileId: integer("profile_id") .notNull() .references(() => linkedinProfiles.id, { onDelete: "cascade" }), degree: varchar("degree", { length: 500 }), school: varchar("school", { length: 500 }), schoolId: varchar("school_id", { length: 255 }), // LinkedIn school ID fieldOfStudy: varchar("field_of_study", { length: 500 }), grade: varchar("grade", { length: 255 }), // Academic grade/GPA description: text("description"), // Education description activitiesAndSocieties: text("activities_and_societies"), // Activities and societies schoolLinkedinUrl: text("school_linkedin_url"), // LinkedIn URL of the school startDate: varchar("start_date", { length: 50 }), endDate: varchar("end_date", { length: 50 }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ profileIdIndex: index("idx_education_profile_id").on(table.profileId), schoolIndex: index("idx_education_school").on(table.school), degreeIndex: index("idx_education_degree").on(table.degree), }) ); // ============================================================================= // COMPREHENSIVE SKILLS ARCHITECTURE // ============================================================================= // Master skills table (ALL skills encountered across the platform) export const skills = pgTable( "skills", { id: serial("id").primaryKey(), name: varchar("name", { length: 255 }).notNull().unique(), category: varchar("category", { length: 100 }), // e.g., 'technical', 'soft', 'industry', 'ai_ml', 'data', 'cloud' description: text("description"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ nameIndex: index("idx_skills_name").on(table.name), categoryIndex: index("idx_skills_category").on(table.category), }) ); // LinkedIn profile skills junction table (profile-level skills) export const linkedinProfileSkills = linkedinSchema.table( "profile_skills", { id: serial("id").primaryKey(), profileId: integer("profile_id") .notNull() .references(() => linkedinProfiles.id, { onDelete: "cascade" }), skillId: integer("skill_id") .notNull() .references(() => skills.id, { onDelete: "cascade" }), endorsementCount: integer("endorsement_count").default(0), skillOrder: integer("skill_order"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ // Unique constraint to prevent duplicate skills per profile profileSkillIndex: unique("uk_profile_skills").on(table.profileId, table.skillId), profileIdIndex: index("idx_profile_skills_profile_id").on(table.profileId), skillIdIndex: index("idx_profile_skills_skill_id").on(table.skillId), endorsementCountIndex: index("idx_profile_skills_endorsement_count").on(table.endorsementCount), }) ); // Work experience skills junction table (job-specific skills) export const workExperienceSkills = linkedinSchema.table( "work_experience_skills", { id: serial("id").primaryKey(), workExperienceId: integer("work_experience_id") .notNull() .references(() => linkedinWorkExperience.id, { onDelete: "cascade" }), skillId: integer("skill_id") .notNull() .references(() => skills.id, { onDelete: "cascade" }), skillOrder: integer("skill_order"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), // Note: updatedAt removed - these are simple associations that don't change }, table => ({ // Unique constraint to prevent duplicate skills per work experience workExperienceSkillIndex: unique("uk_work_experience_skills").on( table.workExperienceId, table.skillId ), workExperienceIdIndex: index("idx_work_experience_skills_work_experience_id").on( table.workExperienceId ), skillIdIndex: index("idx_work_experience_skills_skill_id").on(table.skillId), }) ); // Education skills junction table (education-specific skills) export const educationSkills = linkedinSchema.table( "education_skills", { id: serial("id").primaryKey(), educationId: integer("education_id") .notNull() .references(() => linkedinEducation.id, { onDelete: "cascade" }), skillId: integer("skill_id") .notNull() .references(() => skills.id, { onDelete: "cascade" }), skillOrder: integer("skill_order"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), // Note: updatedAt removed - these are simple associations that don't change }, table => ({ // Unique constraint to prevent duplicate skills per education educationSkillIndex: unique("uk_education_skills").on(table.educationId, table.skillId), educationIdIndex: index("idx_education_skills_education_id").on(table.educationId), skillIdIndex: index("idx_education_skills_skill_id").on(table.skillId), }) ); // Legacy skills table has been completely removed - now using normalized skills architecture // with central public.skills table and junction tables // Note: LinkedIn languages table removed - will be added back later if needed // LinkedIn certifications export const linkedinCertifications = linkedinSchema.table( "certifications", { id: serial("id").primaryKey(), profileId: integer("profile_id") .notNull() .references(() => linkedinProfiles.id, { onDelete: "cascade" }), name: varchar("name", { length: 500 }), organization: varchar("organization", { length: 500 }), url: text("url"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ profileIdIndex: index("idx_certifications_profile_id").on(table.profileId), nameIndex: index("idx_certifications_name").on(table.name), organizationIndex: index("idx_certifications_organization").on(table.organization), }) ); // LinkedIn projects export const linkedinProjects = linkedinSchema.table( "projects", { id: serial("id").primaryKey(), profileId: integer("profile_id") .notNull() .references(() => linkedinProfiles.id, { onDelete: "cascade" }), name: varchar("name", { length: 500 }).notNull(), description: text("description"), // Note: skills moved to junction table if needed in future startDate: varchar("start_date", { length: 50 }), endDate: varchar("end_date", { length: 50 }), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ profileIdIndex: index("idx_projects_profile_id").on(table.profileId), nameIndex: index("idx_projects_name").on(table.name), }) ); // Note: LinkedIn volunteering and recommendations tables removed // They will be added back later when needed // Company tables removed - focusing on profile analysis only // ============================================================================= // CONVERSATIONS, MESSAGES, AND PARTICIPANTS MOVED TO LONG-TERM STORAGE DB // ============================================================================= // These tables have been removed from the main database and moved to a separate // long-term storage database for infrequently accessed data. // ============================================================================= // RELATIONSHIP SCORES TABLE (For LinkedIn Social Account Relationship Analysis) // ============================================================================= export const relationshipScores = pgTable( "relationship_scores", { id: serial("id").primaryKey(), socialAccountIdA: integer("social_account_id_a") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), socialAccountIdB: integer("social_account_id_b") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), score: integer("score").notNull(), // 0-100 relationship strength score modelVersion: varchar("model_version", { length: 20 }).notNull(), // AI model version like v1.2.3, v2.1.0 analysisType: analysisTypeEnum("analysis_type").default("MESSAGE_ANALYSER").notNull(), // Type of analysis performed (MESSAGE_ANALYSER or FEED_ANALYSER) mappingJobId: integer("mapping_job_id").references(() => mappingJobs.id, { onDelete: "cascade", }), // Link to the mapping job that generated this score - cascade delete when mapping job is deleted metadata: jsonb("metadata").default({}).notNull(), // Store analysis metadata including data sources createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ socialAccountAIndex: index("idx_relationship_scores_social_account_a").on( table.socialAccountIdA ), socialAccountBIndex: index("idx_relationship_scores_social_account_b").on( table.socialAccountIdB ), scoreIndex: index("idx_relationship_scores_score").on(table.score), modelVersionIndex: index("idx_relationship_scores_model_version").on(table.modelVersion), mappingJobIdIndex: index("idx_relationship_scores_mapping_job_id").on(table.mappingJobId), // Unique constraint: one score per social account pair per model version per analysis type // Note: We enforce socialAccountIdA < socialAccountIdB to ensure bi-directionality socialAccountPairModelAnalysisUnique: unique( "relationship_scores_social_account_pair_model_analysis_key" ).on(table.socialAccountIdA, table.socialAccountIdB, table.modelVersion, table.analysisType), // Database-level constraint to enforce socialAccountIdA < socialAccountIdB for consistent ordering socialAccountOrderingConstraint: check( "relationship_scores_social_account_ordering_check", sql`social_account_id_a < social_account_id_b` ), }) ); // ============================================================================= // MAPPING JOBS TABLE (For Tracking Mapping Jobs) // ============================================================================= export const mappingJobs = monitoringSchema.table( "mapping_jobs", { id: serial("id").primaryKey(), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), queuedAt: timestamp("queued_at", { withTimezone: true }).defaultNow().notNull(), startedAt: timestamp("started_at", { withTimezone: true }), completedAt: timestamp("completed_at", { withTimezone: true }), metadata: jsonb("metadata").default({}).notNull(), }, table => ({ createdAtIndex: index("idx_mapping_jobs_created_at").on(table.createdAt), queuedAtIndex: index("idx_mapping_jobs_queued_at").on(table.queuedAt), }) ); // Junction table for mapping jobs and social accounts relationship export const mappingJobsSocialAccounts = monitoringSchema.table( "mapping_jobs_social_accounts", { id: serial("id").primaryKey(), mappingJobId: integer("mapping_job_id") .notNull() .references(() => mappingJobs.id, { onDelete: "cascade" }), socialAccountId: integer("social_account_id") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), // Generic metadata for service-specific data (processing stats, metrics, etc.) metadata: jsonb("metadata").default({}).notNull(), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, table => ({ mappingJobIdIndex: index("idx_mapping_jobs_social_accounts_mapping_job_id").on( table.mappingJobId ), socialAccountIdIndex: index("idx_mapping_jobs_social_accounts_social_account_id").on( table.socialAccountId ), createdAtIndex: index("idx_mapping_jobs_social_accounts_created_at").on(table.createdAt), // Unique constraint: one record per mapping job per social account jobSocialAccountUnique: unique("mapping_jobs_social_accounts_job_social_account_key").on( table.mappingJobId, table.socialAccountId ), }) ); // ============================================================================= // ON-DEMAND MAPPING JOBS TABLE (For Chrome Extension Requests) // ============================================================================= export const onDemandMappingJobs = monitoringSchema.table( "on_demand_mapping_jobs", { id: serial("id").primaryKey(), jobKey: varchar("job_key", { length: 16 }).notNull().unique(), // Hash-based business key (includes target identifier) // Request context requesterSocialAccountId: integer("requester_social_account_id") .notNull() .references(() => socialAccounts.id, { onDelete: "cascade" }), // Job tracking - One-to-one relationship: each on-demand job creates exactly one mapping job // Note: Cascade delete should be handled in application logic when deleting on-demand jobs mappingJobId: integer("mapping_job_id").references(() => mappingJobs.id, { onDelete: "cascade", // If mapping job is deleted, delete this on-demand job }), // Lifecycle timestamps createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), // Metadata metadata: jsonb("metadata").default({}).notNull(), }, table => ({ // Primary indexes jobKeyIndex: index("idx_on_demand_mapping_jobs_job_key").on(table.jobKey), requesterIndex: index("idx_on_demand_mapping_jobs_requester").on( table.requesterSocialAccountId ), createdAtIndex: index("idx_on_demand_mapping_jobs_created_at").on(table.createdAt), mappingJobIndex: index("idx_on_demand_mapping_jobs_mapping_job").on(table.mappingJobId), createdAtMappingJobIndex: index("idx_on_demand_mapping_jobs_created_mapping").on( table.createdAt, table.mappingJobId ), }) ); // ============================================================================= // IDEMPOTENCY KEYS TABLE (For Worker Deduplication) // ============================================================================= // Idempotency table removed for MVP - can be added back when needed for retries/duplicates // ============================================================================= // RELATIONS // ============================================================================= export const usersRelations = relations(users, ({ many }) => ({ contactInfos: many(contactInfos), socialAccounts: many(socialAccounts), authenticatedUsers: many(authenticatedUsers), unipileAccounts: many(unipileAccounts), })); export const contactInfosRelations = relations(contactInfos, ({ one }) => ({ user: one(users, { fields: [contactInfos.userId], references: [users.id], }), })); export const socialAccountsRelations = relations(socialAccounts, ({ one, many }) => ({ user: one(users, { fields: [socialAccounts.userId], references: [users.id], }), unipileAccount: one(unipileAccounts, { fields: [socialAccounts.id], references: [unipileAccounts.socialAccountId], }), linkedinProfile: one(linkedinProfiles, { fields: [socialAccounts.id], references: [linkedinProfiles.socialAccountId], }), relationshipScoresAsSocialAccountA: many(relationshipScores, { relationName: "socialAccountARelationships", }), relationshipScoresAsSocialAccountB: many(relationshipScores, { relationName: "socialAccountBRelationships", }), mappingJobsSocialAccounts: many(mappingJobsSocialAccounts), onDemandMappingJobs: many(onDemandMappingJobs), })); export const authenticatedUsersRelations = relations(authenticatedUsers, ({ one }) => ({ user: one(users, { fields: [authenticatedUsers.userId], references: [users.id], }), })); export const unipileAccountsRelations = relations(unipileAccounts, ({ one }) => ({ socialAccount: one(socialAccounts, { fields: [unipileAccounts.socialAccountId], references: [socialAccounts.id], }), })); // LinkedIn profile relations export const linkedinProfilesRelations = relations(linkedinProfiles, ({ one, many }) => ({ socialAccount: one(socialAccounts, { fields: [linkedinProfiles.socialAccountId], references: [socialAccounts.id], }), // Note: contactInfos now in main contact_infos table with source="linkedin" workExperience: many(linkedinWorkExperience), education: many(linkedinEducation), profileSkills: many(linkedinProfileSkills), // New normalized skills // skills: Removed deprecated table - now using normalized profileSkills junction // Note: languages removed - will be added back later if needed certifications: many(linkedinCertifications), projects: many(linkedinProjects), // Note: volunteering and recommendations removed - will be added back later })); // Note: linkedinContactInfosRelations removed - using main contact_infos table now export const linkedinWorkExperienceRelations = relations( linkedinWorkExperience, ({ one, many }) => ({ profile: one(linkedinProfiles, { fields: [linkedinWorkExperience.profileId], references: [linkedinProfiles.id], }), workExperienceSkills: many(workExperienceSkills), }) ); export const linkedinEducationRelations = relations(linkedinEducation, ({ one, many }) => ({ profile: one(linkedinProfiles, { fields: [linkedinEducation.profileId], references: [linkedinProfiles.id], }), educationSkills: many(educationSkills), })); // ============================================================================= // NORMALIZED SKILLS RELATIONS // ============================================================================= export const skillsRelations = relations(skills, ({ many }) => ({ profileSkills: many(linkedinProfileSkills), workExperienceSkills: many(workExperienceSkills), educationSkills: many(educationSkills), })); export const linkedinProfileSkillsRelations = relations(linkedinProfileSkills, ({ one }) => ({ profile: one(linkedinProfiles, { fields: [linkedinProfileSkills.profileId], references: [linkedinProfiles.id], }), skill: one(skills, { fields: [linkedinProfileSkills.skillId], references: [skills.id], }), })); export const workExperienceSkillsRelations = relations(workExperienceSkills, ({ one }) => ({ workExperience: one(linkedinWorkExperience, { fields: [workExperienceSkills.workExperienceId], references: [linkedinWorkExperience.id], }), skill: one(skills, { fields: [workExperienceSkills.skillId], references: [skills.id], }), })); export const educationSkillsRelations = relations(educationSkills, ({ one }) => ({ education: one(linkedinEducation, { fields: [educationSkills.educationId], references: [linkedinEducation.id], }), skill: one(skills, { fields: [educationSkills.skillId], references: [skills.id], }), })); // Legacy skills relations removed - now using normalized skills architecture // Note: linkedinLanguagesRelations removed - will be added back later if needed export const linkedinCertificationsRelations = relations(linkedinCertifications, ({ one }) => ({ profile: one(linkedinProfiles, { fields: [linkedinCertifications.profileId], references: [linkedinProfiles.id], }), })); export const linkedinProjectsRelations = relations(linkedinProjects, ({ one }) => ({ profile: one(linkedinProfiles, { fields: [linkedinProjects.profileId], references: [linkedinProfiles.id], }), })); // Note: Relations for linkedinVolunteering and linkedinRecommendations removed // They will be added back later when needed // Relations for conversations, messages, and participants removed // (moved to long-term storage database) export const relationshipScoresRelations = relations(relationshipScores, ({ one }) => ({ socialAccountA: one(socialAccounts, { fields: [relationshipScores.socialAccountIdA], references: [socialAccounts.id], relationName: "socialAccountARelationships", }), socialAccountB: one(socialAccounts, { fields: [relationshipScores.socialAccountIdB], references: [socialAccounts.id], relationName: "socialAccountBRelationships", }), mappingJob: one(mappingJobs, { fields: [relationshipScores.mappingJobId], references: [mappingJobs.id], }), })); // Mapping service jobs relations export const mappingJobsRelations = relations(mappingJobs, ({ many }) => ({ mappingJobsSocialAccounts: many(mappingJobsSocialAccounts), relationshipScores: many(relationshipScores), onDemandMappingJobs: many(onDemandMappingJobs), })); export const mappingJobsSocialAccountsRelations = relations( mappingJobsSocialAccounts, ({ one }) => ({ mappingJob: one(mappingJobs, { fields: [mappingJobsSocialAccounts.mappingJobId], references: [mappingJobs.id], }), socialAccount: one(socialAccounts, { fields: [mappingJobsSocialAccounts.socialAccountId], references: [socialAccounts.id], }), }) ); export const onDemandMappingJobsRelations = relations(onDemandMappingJobs, ({ one }) => ({ requesterSocialAccount: one(socialAccounts, { fields: [onDemandMappingJobs.requesterSocialAccountId], references: [socialAccounts.id], }), mappingJob: one(mappingJobs, { fields: [onDemandMappingJobs.mappingJobId], references: [mappingJobs.id], }), })); // ============================================================================= // TYPESCRIPT TYPES (Auto-generated by Drizzle) // ============================================================================= // Select types (what you get when querying) export type User = typeof users.$inferSelect; export type ContactInfo = typeof contactInfos.$inferSelect; export type SocialAccount = typeof socialAccounts.$inferSelect; export type AuthenticatedUser = typeof authenticatedUsers.$inferSelect; export type UnipileAccount = typeof unipileAccounts.$inferSelect; export type RelationshipScore = typeof relationshipScores.$inferSelect; export type MappingJob = typeof mappingJobs.$inferSelect; export type MappingJobSocialAccount = typeof mappingJobsSocialAccounts.$inferSelect; export type OnDemandMappingJob = typeof onDemandMappingJobs.$inferSelect; // LinkedIn profile types export type LinkedinProfile = typeof linkedinProfiles.$inferSelect; // Note: LinkedinContactInfo type removed - using ContactInfo with source="linkedin" export type LinkedinWorkExperience = typeof linkedinWorkExperience.$inferSelect; export type LinkedinEducation = typeof linkedinEducation.$inferSelect; // Normalized skills types export type Skill = typeof skills.$inferSelect; export type LinkedinProfileSkill = typeof linkedinProfileSkills.$inferSelect; export type WorkExperienceSkill = typeof workExperienceSkills.$inferSelect; export type EducationSkill = typeof educationSkills.$inferSelect; // Legacy skills type removed - now using normalized Skill, LinkedinProfileSkill, WorkExperienceSkill // Note: LinkedinLanguage type removed - will be added back later if needed export type LinkedinCertification = typeof linkedinCertifications.$inferSelect; export type LinkedinProject = typeof linkedinProjects.$inferSelect; // Note: LinkedinVolunteering and LinkedinRecommendation types removed - will be added back later // Conversation, Message, and ConversationParticipant types moved to long-term storage database // Insert types (what you need when inserting) export type NewUser = typeof users.$inferInsert; export type NewContactInfo = typeof contactInfos.$inferInsert; export type NewSocialAccount = typeof socialAccounts.$inferInsert; export type NewAuthenticatedUser = typeof authenticatedUsers.$inferInsert; export type NewUnipileAccount = typeof unipileAccounts.$inferInsert; export type NewRelationshipScore = typeof relationshipScores.$inferInsert; export type NewMappingJob = typeof mappingJobs.$inferInsert; export type NewMappingJobSocialAccount = typeof mappingJobsSocialAccounts.$inferInsert; // LinkedIn profile insert types export type NewLinkedinProfile = typeof linkedinProfiles.$inferInsert; // Note: NewLinkedinContactInfo type removed - using NewContactInfo with source="linkedin" export type NewLinkedinWorkExperience = typeof linkedinWorkExperience.$inferInsert; export type NewLinkedinEducation = typeof linkedinEducation.$inferInsert; // Normalized skills insert types export type NewSkill = typeof skills.$inferInsert; export type NewLinkedinProfileSkill = typeof linkedinProfileSkills.$inferInsert; export type NewWorkExperienceSkill = typeof workExperienceSkills.$inferInsert; export type NewEducationSkill = typeof educationSkills.$inferInsert; // Legacy skills insert type removed - now using normalized NewSkill, NewLinkedinProfileSkill, NewWorkExperienceSkill, NewProjectSkill // Note: NewLinkedinLanguage type removed - will be added back later if needed export type NewLinkedinCertification = typeof linkedinCertifications.$inferInsert; export type NewLinkedinProject = typeof linkedinProjects.$inferInsert; // Note: NewLinkedinVolunteering and NewLinkedinRecommendation types removed - will be added back later // Conversation, Message, and ConversationParticipant insert types moved to long-term storage database // Enum types export type SocialPlatformType = (typeof socialPlatformTypeEnum.enumValues)[number]; export type ContactInfoType = (typeof contactInfoTypeEnum.enumValues)[number]; export type ContactInfoSource = (typeof contactInfoSourceEnum.enumValues)[number]; export type SocialAccountType = (typeof socialAccountTypeEnum.enumValues)[number]; export type UnipileAccountType = (typeof unipileAccountTypeEnum.enumValues)[number]; // LinkedIn specific enum types export type LinkedinInvitationType = (typeof linkedinInvitationTypeEnum.enumValues)[number]; export type LinkedinInvitationStatus = (typeof linkedinInvitationStatusEnum.enumValues)[number]; export type LinkedinNetworkDistance = (typeof linkedinNetworkDistanceEnum.enumValues)[number]; export type ProficiencyLevel = (typeof proficiencyLevelEnum.enumValues)[number]; // Analysis type enum export type AnalysisType = (typeof analysisTypeEnum.enumValues)[number]; // Mapping type enum export type MappingType = (typeof mappingTypeEnum.enumValues)[number]; // MessageType moved to long-term storage database // ============================================================================= // QUERY HELPERS (Optional - for common queries) // ============================================================================= // Common query patterns that will be fully typed export const userWithContactsAndAccounts = { id: true, givenName: true, familyName: true, createdAt: true, contactInfos: { id: true, type: true, value: true, metadata: true, createdAt: true, }, socialAccounts: { id: true, platform: true, internalIdentifier: true, publicIdentifier: true, createdAt: true, }, authenticatedUsers: { id: true, cognitoUserId: true, authProvider: true, createdAt: true, }, unipileAccounts: { id: true, unipileAccountId: true, platformType: true, isActive: true, connectedAt: true, lastSyncedAt: true, metadata: true, }, }; // Conversation query helpers moved to long-term storage database export const socialAccountWithRelationships = { id: true, userId: true, platform: true, internalIdentifier: true, publicIdentifier: true, createdAt: true, user: { id: true, givenName: true, familyName: true, contactInfos: { id: true, type: true, value: true, }, }, relationshipScoresAsSocialAccountA: { id: true, score: true, modelVersion: true, createdAt: true, socialAccountB: { id: true, platform: true, internalIdentifier: true, publicIdentifier: true, user: { id: true, givenName: true, familyName: true, contactInfos: { id: true, type: true, value: true, }, }, }, }, relationshipScoresAsSocialAccountB: { id: true, score: true, modelVersion: true, createdAt: true, socialAccountA: { id: true, platform: true, internalIdentifier: true, publicIdentifier: true, user: { id: true, givenName: true, familyName: true, contactInfos: { id: true, type: true, value: true, }, }, }, }, };