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
text/typescript
/**
* =============================================================================
* 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,
},
},
},
},
};