import { pgTable, text, timestamp, integer, jsonb, boolean, index, primaryKey, uniqueIndex, } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; /** * Skills table - main entity storing indexed skills */ export const skills = pgTable( 'skills', { // Primary key: owner/repo/skill-name id: text('id').primaryKey(), name: text('name').notNull(), description: text('description').notNull(), // Source information githubOwner: text('github_owner').notNull(), githubRepo: text('github_repo').notNull(), skillPath: text('skill_path').notNull(), branch: text('branch').default('main'), commitSha: text('commit_sha'), // Source format (which platform's instruction file format) sourceFormat: text('source_format').default('skill.md'), // Metadata version: text('version'), license: text('license'), author: text('author'), homepage: text('homepage'), compatibility: jsonb('compatibility').$type<{ platforms?: string[]; requires?: string[]; minVersion?: string; }>(), triggers: jsonb('triggers').$type<{ filePatterns?: string[]; keywords?: string[]; languages?: string[]; }>(), // Quality signals githubStars: integer('github_stars').default(0), githubForks: integer('github_forks').default(0), downloadCount: integer('download_count').default(0), viewCount: integer('view_count').default(0), // Ratings rating: integer('rating'), // 1-5 average ratingCount: integer('rating_count').default(0), ratingSum: integer('rating_sum').default(0), // Security securityScore: integer('security_score'), // 0-100 (deprecated, use securityStatus) securityStatus: text('security_status').$type<'pass' | 'warning' | 'fail'>(), // PASS, WARNING, FAIL isVerified: boolean('is_verified').default(false), isFeatured: boolean('is_featured').default(false), isBlocked: boolean('is_blocked').default(false), // Blocked from re-indexing (owner requested removal) lastScanned: timestamp('last_scanned'), // Curation (populated by batch scripts, not crawler) qualityScore: integer('quality_score'), // 0-100 from analyzer qualityDetails: jsonb('quality_details').$type<{ documentation: number; maintenance: number; popularity: number; factors: Array<{ name: string; score: number; weight: number; details?: string }>; }>(), skillType: text('skill_type').$type<'standalone' | 'project-bound' | 'collection' | 'aggregator'>(), isDuplicate: boolean('is_duplicate').default(false), canonicalSkillId: text('canonical_skill_id'), // points to the "original" if this is a duplicate repoSkillCount: integer('repo_skill_count'), // cached count of skills in same repo // Content (cached) contentHash: text('content_hash'), rawContent: text('raw_content'), // Cached skill files (populated on first download) cachedFiles: jsonb('cached_files').$type<{ fetchedAt: string; // ISO timestamp when files were fetched commitSha: string; // Git commit SHA for cache invalidation totalSize: number; // Total size in bytes items: Array<{ name: string; // e.g., "SKILL.md", "setup.sh" path: string; // Relative path from skill root content: string; // File content (base64 for binary) size: number; // File size in bytes isBinary: boolean; // Whether content is base64 encoded }>; }>(), // Timestamps createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), indexedAt: timestamp('indexed_at'), lastDownloadedAt: timestamp('last_downloaded_at'), }, (table) => ({ nameIdx: index('idx_skills_name').on(table.name), ownerIdx: index('idx_skills_owner').on(table.githubOwner), starsIdx: index('idx_skills_stars').on(table.githubStars), securityIdx: index('idx_skills_security').on(table.securityScore), securityStatusIdx: index('idx_skills_security_status').on(table.securityStatus), verifiedIdx: index('idx_skills_verified').on(table.isVerified), featuredIdx: index('idx_skills_featured').on(table.isFeatured), blockedIdx: index('idx_skills_blocked').on(table.isBlocked), updatedIdx: index('idx_skills_updated').on(table.updatedAt), sourceFormatIdx: index('idx_skills_source_format').on(table.sourceFormat), lastDownloadedIdx: index('idx_skills_last_downloaded').on(table.lastDownloadedAt), qualityIdx: index('idx_skills_quality').on(table.qualityScore), skillTypeIdx: index('idx_skills_type').on(table.skillType), duplicateIdx: index('idx_skills_duplicate').on(table.isDuplicate), contentHashIdx: index('idx_skills_content_hash').on(table.contentHash), }) ); /** * Categories for organizing skills */ export const categories = pgTable( 'categories', { id: text('id').primaryKey(), name: text('name').notNull(), slug: text('slug').notNull().unique(), description: text('description'), icon: text('icon'), color: text('color'), parentId: text('parent_id'), sortOrder: integer('sort_order').default(0), skillCount: integer('skill_count').default(0), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ slugIdx: uniqueIndex('idx_categories_slug').on(table.slug), }) ); /** * Many-to-many relationship between skills and categories */ export const skillCategories = pgTable( 'skill_categories', { skillId: text('skill_id') .references(() => skills.id, { onDelete: 'cascade' }) .notNull(), categoryId: text('category_id') .references(() => categories.id, { onDelete: 'cascade' }) .notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.skillId, table.categoryId] }), skillIdx: index('idx_skill_categories_skill').on(table.skillId), categoryIdx: index('idx_skill_categories_category').on(table.categoryId), }) ); /** * Users (authenticated via GitHub OAuth) */ export const users = pgTable( 'users', { id: text('id').primaryKey(), githubId: text('github_id').unique().notNull(), username: text('username').notNull(), displayName: text('display_name'), email: text('email'), avatarUrl: text('avatar_url'), bio: text('bio'), preferredLocale: text('preferred_locale'), isAdmin: boolean('is_admin').default(false), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), lastLoginAt: timestamp('last_login_at'), }, (table) => ({ githubIdx: uniqueIndex('idx_users_github').on(table.githubId), usernameIdx: index('idx_users_username').on(table.username), }) ); /** * User ratings and reviews for skills */ export const ratings = pgTable( 'ratings', { id: text('id').primaryKey(), skillId: text('skill_id') .references(() => skills.id, { onDelete: 'cascade' }) .notNull(), userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), rating: integer('rating').notNull(), // 1-5 review: text('review'), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => ({ skillIdx: index('idx_ratings_skill').on(table.skillId), userIdx: index('idx_ratings_user').on(table.userId), userSkillIdx: uniqueIndex('idx_ratings_user_skill').on(table.userId, table.skillId), }) ); /** * Anonymous installation tracking */ export const installations = pgTable( 'installations', { id: text('id').primaryKey(), skillId: text('skill_id') .references(() => skills.id, { onDelete: 'cascade' }) .notNull(), platform: text('platform').notNull(), // claude, codex, copilot method: text('method'), // cli, web, desktop createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ skillIdx: index('idx_installations_skill').on(table.skillId), platformIdx: index('idx_installations_platform').on(table.platform), createdIdx: index('idx_installations_created').on(table.createdAt), }) ); /** * User favorites/bookmarks */ export const favorites = pgTable( 'favorites', { userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), skillId: text('skill_id') .references(() => skills.id, { onDelete: 'cascade' }) .notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.userId, table.skillId] }), userIdx: index('idx_favorites_user').on(table.userId), skillIdx: index('idx_favorites_skill').on(table.skillId), }) ); /** * Indexing job queue status */ export const indexingJobs = pgTable( 'indexing_jobs', { id: text('id').primaryKey(), type: text('type').notNull(), // full-crawl, incremental, single-skill status: text('status').notNull(), // pending, running, completed, failed skillId: text('skill_id'), startedAt: timestamp('started_at'), completedAt: timestamp('completed_at'), error: text('error'), metadata: jsonb('metadata').$type>(), createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ statusIdx: index('idx_indexing_jobs_status').on(table.status), typeIdx: index('idx_indexing_jobs_type').on(table.type), }) ); /** * Discovered repositories - tracks repos found by various discovery strategies * Used to queue repos for deep scanning and track discovery sources */ export const discoveredRepos = pgTable( 'discovered_repos', { id: text('id').primaryKey(), // owner/repo owner: text('owner').notNull(), repo: text('repo').notNull(), discoveredVia: text('discovered_via').notNull(), // 'awesome-list', 'topic-search', 'fork', 'org-scan', 'code-search' sourceUrl: text('source_url'), // URL of the awesome list or search query that found this repo lastScanned: timestamp('last_scanned'), skillCount: integer('skill_count').default(0), hasSkillMd: boolean('has_skill_md').default(false), githubStars: integer('github_stars').default(0), githubForks: integer('github_forks').default(0), defaultBranch: text('default_branch').default('main'), isArchived: boolean('is_archived').default(false), scanError: text('scan_error'), // Last scan error if any createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), }, (table) => ({ ownerIdx: index('idx_discovered_repos_owner').on(table.owner), discoveredViaIdx: index('idx_discovered_repos_discovered_via').on(table.discoveredVia), lastScannedIdx: index('idx_discovered_repos_last_scanned').on(table.lastScanned), skillCountIdx: index('idx_discovered_repos_skill_count').on(table.skillCount), hasSkillMdIdx: index('idx_discovered_repos_has_skill_md').on(table.hasSkillMd), }) ); /** * Awesome lists - tracks curated lists that we crawl for repo discovery */ export const awesomeLists = pgTable( 'awesome_lists', { id: text('id').primaryKey(), // owner/repo owner: text('owner').notNull(), repo: text('repo').notNull(), name: text('name'), lastParsed: timestamp('last_parsed'), repoCount: integer('repo_count').default(0), // Number of repos found in this list isActive: boolean('is_active').default(true), // Whether to continue crawling this list createdAt: timestamp('created_at').defaultNow().notNull(), }, (table) => ({ lastParsedIdx: index('idx_awesome_lists_last_parsed').on(table.lastParsed), }) ); /** * Removal requests - allows repo owners to request their skills be removed */ export const removalRequests = pgTable( 'removal_requests', { id: text('id').primaryKey(), userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), skillId: text('skill_id').notNull(), // Can reference non-existent skill if already removed reason: text('reason').notNull(), status: text('status').notNull().default('pending'), // pending, approved, rejected verifiedOwner: boolean('verified_owner').default(false), // GitHub API verification result createdAt: timestamp('created_at').defaultNow().notNull(), resolvedAt: timestamp('resolved_at'), resolvedBy: text('resolved_by').references(() => users.id), resolutionNote: text('resolution_note'), }, (table) => ({ userIdx: index('idx_removal_requests_user').on(table.userId), skillIdx: index('idx_removal_requests_skill').on(table.skillId), statusIdx: index('idx_removal_requests_status').on(table.status), }) ); /** * Add requests - allows users to request new skills be indexed */ export const addRequests = pgTable( 'add_requests', { id: text('id').primaryKey(), userId: text('user_id') .references(() => users.id, { onDelete: 'cascade' }) .notNull(), repositoryUrl: text('repository_url').notNull(), // Full GitHub URL skillPath: text('skill_path'), // Optional path within repo (for subfolder skills) reason: text('reason').notNull(), // Why should this skill be added status: text('status').notNull().default('pending'), // pending, approved, rejected, indexed validRepo: boolean('valid_repo').default(false), // GitHub API validation result hasSkillMd: boolean('has_skill_md').default(false), // Whether SKILL.md was found createdAt: timestamp('created_at').defaultNow().notNull(), processedAt: timestamp('processed_at'), indexedSkillId: text('indexed_skill_id'), // Reference to skill if successfully indexed errorMessage: text('error_message'), // Error if indexing failed }, (table) => ({ userIdx: index('idx_add_requests_user').on(table.userId), statusIdx: index('idx_add_requests_status').on(table.status), repoIdx: index('idx_add_requests_repo').on(table.repositoryUrl), }) ); // Relations export const skillsRelations = relations(skills, ({ many }) => ({ categories: many(skillCategories), ratings: many(ratings), installations: many(installations), favorites: many(favorites), })); export const categoriesRelations = relations(categories, ({ many, one }) => ({ skills: many(skillCategories), parent: one(categories, { fields: [categories.parentId], references: [categories.id], }), })); export const skillCategoriesRelations = relations(skillCategories, ({ one }) => ({ skill: one(skills, { fields: [skillCategories.skillId], references: [skills.id], }), category: one(categories, { fields: [skillCategories.categoryId], references: [categories.id], }), })); export const usersRelations = relations(users, ({ many }) => ({ ratings: many(ratings), favorites: many(favorites), removalRequests: many(removalRequests), addRequests: many(addRequests), })); export const ratingsRelations = relations(ratings, ({ one }) => ({ skill: one(skills, { fields: [ratings.skillId], references: [skills.id], }), user: one(users, { fields: [ratings.userId], references: [users.id], }), })); export const installationsRelations = relations(installations, ({ one }) => ({ skill: one(skills, { fields: [installations.skillId], references: [skills.id], }), })); export const favoritesRelations = relations(favorites, ({ one }) => ({ user: one(users, { fields: [favorites.userId], references: [users.id], }), skill: one(skills, { fields: [favorites.skillId], references: [skills.id], }), })); export const removalRequestsRelations = relations(removalRequests, ({ one }) => ({ user: one(users, { fields: [removalRequests.userId], references: [users.id], }), resolver: one(users, { fields: [removalRequests.resolvedBy], references: [users.id], }), })); export const addRequestsRelations = relations(addRequests, ({ one }) => ({ user: one(users, { fields: [addRequests.userId], references: [users.id], }), })); /** * Email subscriptions for newsletter and marketing emails */ export const emailSubscriptions = pgTable( 'email_subscriptions', { id: text('id').primaryKey(), email: text('email').notNull().unique(), source: text('source').notNull(), // 'oauth', 'newsletter', 'claim', 'early-access' marketingConsent: boolean('marketing_consent').default(false), consentDate: timestamp('consent_date'), createdAt: timestamp('created_at').defaultNow().notNull(), unsubscribedAt: timestamp('unsubscribed_at'), }, (table) => ({ emailIdx: uniqueIndex('idx_email_subscriptions_email').on(table.email), sourceIdx: index('idx_email_subscriptions_source').on(table.source), }) );