2026-01-19 18:39:14 +00:00

101 lines
3.9 KiB
TypeScript

/**
* Database schema using Drizzle ORM with SQLite.
*/
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const jobs = sqliteTable('jobs', {
id: text('id').primaryKey(),
// From crawler
source: text('source', { enum: ['gradcracker', 'indeed', 'linkedin', 'ukvisajobs', 'manual'] }).notNull().default('gradcracker'),
sourceJobId: text('source_job_id'),
jobUrlDirect: text('job_url_direct'),
datePosted: text('date_posted'),
title: text('title').notNull(),
employer: text('employer').notNull(),
employerUrl: text('employer_url'),
jobUrl: text('job_url').notNull().unique(),
applicationLink: text('application_link'),
disciplines: text('disciplines'),
deadline: text('deadline'),
salary: text('salary'),
location: text('location'),
degreeRequired: text('degree_required'),
starting: text('starting'),
jobDescription: text('job_description'),
// JobSpy fields (nullable for other sources)
jobType: text('job_type'),
salarySource: text('salary_source'),
salaryInterval: text('salary_interval'),
salaryMinAmount: real('salary_min_amount'),
salaryMaxAmount: real('salary_max_amount'),
salaryCurrency: text('salary_currency'),
isRemote: integer('is_remote', { mode: 'boolean' }),
jobLevel: text('job_level'),
jobFunction: text('job_function'),
listingType: text('listing_type'),
emails: text('emails'),
companyIndustry: text('company_industry'),
companyLogo: text('company_logo'),
companyUrlDirect: text('company_url_direct'),
companyAddresses: text('company_addresses'),
companyNumEmployees: text('company_num_employees'),
companyRevenue: text('company_revenue'),
companyDescription: text('company_description'),
skills: text('skills'),
experienceRange: text('experience_range'),
companyRating: real('company_rating'),
companyReviewsCount: integer('company_reviews_count'),
vacancyCount: integer('vacancy_count'),
workFromHomeType: text('work_from_home_type'),
// Orchestrator enrichments
status: text('status', {
enum: ['discovered', 'processing', 'ready', 'applied', 'skipped', 'expired']
}).notNull().default('discovered'),
suitabilityScore: real('suitability_score'),
suitabilityReason: text('suitability_reason'),
tailoredSummary: text('tailored_summary'),
tailoredHeadline: text('tailored_headline'),
tailoredSkills: text('tailored_skills'),
selectedProjectIds: text('selected_project_ids'),
pdfPath: text('pdf_path'),
notionPageId: text('notion_page_id'),
// Timestamps
discoveredAt: text('discovered_at').notNull().default(sql`(datetime('now'))`),
processedAt: text('processed_at'),
appliedAt: text('applied_at'),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
});
export const pipelineRuns = sqliteTable('pipeline_runs', {
id: text('id').primaryKey(),
startedAt: text('started_at').notNull().default(sql`(datetime('now'))`),
completedAt: text('completed_at'),
status: text('status', {
enum: ['running', 'completed', 'failed']
}).notNull().default('running'),
jobsDiscovered: integer('jobs_discovered').notNull().default(0),
jobsProcessed: integer('jobs_processed').notNull().default(0),
errorMessage: text('error_message'),
});
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value').notNull(),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
});
export type JobRow = typeof jobs.$inferSelect;
export type NewJobRow = typeof jobs.$inferInsert;
export type PipelineRunRow = typeof pipelineRuns.$inferSelect;
export type NewPipelineRunRow = typeof pipelineRuns.$inferInsert;
export type SettingsRow = typeof settings.$inferSelect;
export type NewSettingsRow = typeof settings.$inferInsert;