import { z } from "zod"; import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core"; import { sql } from "drizzle-orm"; // ===== Zod Validation Schemas ===== export const userSchema = z.object({ id: z.string(), username: z.string(), password: z.string(), email: z.email(), emailVerified: z.boolean().default(false), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); export const githubConfigSchema = z.object({ owner: z.string(), type: z.enum(["personal", "organization"]), token: z.string(), includeStarred: z.boolean().default(false), includeForks: z.boolean().default(true), skipForks: z.boolean().default(false), includeArchived: z.boolean().default(false), includePrivate: z.boolean().default(true), includePublic: z.boolean().default(true), includeOrganizations: z.array(z.string()).default([]), starredReposOrg: z.string().optional(), mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"), defaultOrg: z.string().optional(), starredCodeOnly: z.boolean().default(false), skipStarredIssues: z.boolean().optional(), // Deprecated: kept for backward compatibility, use starredCodeOnly instead starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(), }); export const giteaConfigSchema = z.object({ url: z.url(), token: z.string(), defaultOwner: z.string(), organization: z.string().optional(), mirrorInterval: z.string().default("8h"), lfs: z.boolean().default(false), wiki: z.boolean().default(false), visibility: z .enum(["public", "private", "limited", "default"]) .default("default"), createOrg: z.boolean().default(true), templateOwner: z.string().optional(), templateRepo: z.string().optional(), addTopics: z.boolean().default(true), topicPrefix: z.string().optional(), preserveVisibility: z.boolean().default(true), preserveOrgStructure: z.boolean().default(false), forkStrategy: z .enum(["skip", "reference", "full-copy"]) .default("reference"), // Mirror options issueConcurrency: z.number().int().min(1).default(3), pullRequestConcurrency: z.number().int().min(1).default(5), mirrorReleases: z.boolean().default(false), releaseLimit: z.number().default(10), mirrorMetadata: z.boolean().default(false), mirrorIssues: z.boolean().default(false), mirrorPullRequests: z.boolean().default(false), mirrorLabels: z.boolean().default(false), mirrorMilestones: z.boolean().default(false), }); export const scheduleConfigSchema = z.object({ enabled: z.boolean().default(false), interval: z.string().default("0 2 * * *"), concurrent: z.boolean().default(false), batchSize: z.number().default(10), pauseBetweenBatches: z.number().default(5000), retryAttempts: z.number().default(3), retryDelay: z.number().default(60000), timeout: z.number().default(3600000), autoRetry: z.boolean().default(true), cleanupBeforeMirror: z.boolean().default(false), notifyOnFailure: z.boolean().default(true), notifyOnSuccess: z.boolean().default(false), logLevel: z.enum(["error", "warn", "info", "debug"]).default("info"), timezone: z.string().default("UTC"), onlyMirrorUpdated: z.boolean().default(false), updateInterval: z.number().default(86400000), skipRecentlyMirrored: z.boolean().default(true), recentThreshold: z.number().default(3600000), autoImport: z.boolean().default(true), autoMirror: z.boolean().default(false), lastRun: z.coerce.date().optional(), nextRun: z.coerce.date().optional(), }); export const cleanupConfigSchema = z.object({ enabled: z.boolean().default(false), retentionDays: z.number().default(604800), // 7 days in seconds deleteFromGitea: z.boolean().default(false), deleteIfNotInGitHub: z.boolean().default(true), protectedRepos: z.array(z.string()).default([]), dryRun: z.boolean().default(false), orphanedRepoAction: z .enum(["skip", "archive", "delete"]) .default("archive"), batchSize: z.number().default(10), pauseBetweenDeletes: z.number().default(2000), lastRun: z.coerce.date().optional(), nextRun: z.coerce.date().optional(), }); export const configSchema = z.object({ id: z.string(), userId: z.string(), name: z.string(), isActive: z.boolean().default(true), githubConfig: githubConfigSchema, giteaConfig: giteaConfigSchema, include: z.array(z.string()).default(["*"]), exclude: z.array(z.string()).default([]), scheduleConfig: scheduleConfigSchema, cleanupConfig: cleanupConfigSchema, createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); export const repositorySchema = z.object({ id: z.string(), userId: z.string(), configId: z.string(), name: z.string(), fullName: z.string(), normalizedFullName: z.string(), url: z.url(), cloneUrl: z.url(), owner: z.string(), organization: z.string().optional().nullable(), mirroredLocation: z.string().default(""), isPrivate: z.boolean().default(false), isForked: z.boolean().default(false), forkedFrom: z.string().optional().nullable(), hasIssues: z.boolean().default(false), isStarred: z.boolean().default(false), isArchived: z.boolean().default(false), size: z.number().default(0), hasLFS: z.boolean().default(false), hasSubmodules: z.boolean().default(false), language: z.string().optional().nullable(), description: z.string().optional().nullable(), defaultBranch: z.string(), visibility: z.enum(["public", "private", "internal"]).default("public"), status: z .enum([ "imported", "mirroring", "mirrored", "failed", "skipped", "ignored", // User explicitly wants to ignore this repository "deleting", "deleted", "syncing", "synced", "archived", ]) .default("imported"), lastMirrored: z.coerce.date().optional().nullable(), errorMessage: z.string().optional().nullable(), destinationOrg: z.string().optional().nullable(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); export const mirrorJobSchema = z.object({ id: z.string(), userId: z.string(), repositoryId: z.string().optional().nullable(), repositoryName: z.string().optional().nullable(), organizationId: z.string().optional().nullable(), organizationName: z.string().optional().nullable(), details: z.string().optional().nullable(), status: z .enum([ "imported", "mirroring", "mirrored", "failed", "skipped", "ignored", // User explicitly wants to ignore this repository "deleting", "deleted", "syncing", "synced", "archived", ]) .default("imported"), message: z.string(), timestamp: z.coerce.date(), jobType: z.enum(["mirror", "cleanup", "import"]).default("mirror"), batchId: z.string().optional().nullable(), totalItems: z.number().optional().nullable(), completedItems: z.number().default(0), itemIds: z.array(z.string()).optional().nullable(), completedItemIds: z.array(z.string()).default([]), inProgress: z.boolean().default(false), startedAt: z.coerce.date().optional().nullable(), completedAt: z.coerce.date().optional().nullable(), lastCheckpoint: z.coerce.date().optional().nullable(), }); export const organizationSchema = z.object({ id: z.string(), userId: z.string(), configId: z.string(), name: z.string(), normalizedName: z.string(), avatarUrl: z.string(), membershipRole: z.enum(["member", "admin", "owner", "billing_manager"]).default("member"), isIncluded: z.boolean().default(true), destinationOrg: z.string().optional().nullable(), status: z .enum([ "imported", "mirroring", "mirrored", "failed", "skipped", "ignored", // User explicitly wants to ignore this repository "deleting", "deleted", "syncing", "synced", ]) .default("imported"), lastMirrored: z.coerce.date().optional().nullable(), errorMessage: z.string().optional().nullable(), repositoryCount: z.number().default(0), publicRepositoryCount: z.number().optional(), privateRepositoryCount: z.number().optional(), forkRepositoryCount: z.number().optional(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); export const eventSchema = z.object({ id: z.string(), userId: z.string(), channel: z.string(), payload: z.any(), read: z.boolean().default(false), createdAt: z.coerce.date(), }); // ===== Drizzle Table Definitions ===== export const users = sqliteTable("users", { id: text("id").primaryKey(), name: text("name"), email: text("email").notNull().unique(), emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false), image: text("image"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), // Custom fields username: text("username"), }, (_table) => []); export const events = sqliteTable("events", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), channel: text("channel").notNull(), payload: text("payload", { mode: "json" }).notNull(), read: integer("read", { mode: "boolean" }).notNull().default(false), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_events_user_channel").on(table.userId, table.channel), index("idx_events_created_at").on(table.createdAt), index("idx_events_read").on(table.read), ]); export const configs = sqliteTable("configs", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), name: text("name").notNull(), isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), githubConfig: text("github_config", { mode: "json" }) .$type>() .notNull(), giteaConfig: text("gitea_config", { mode: "json" }) .$type>() .notNull(), include: text("include", { mode: "json" }) .$type() .notNull() .default(sql`'["*"]'`), exclude: text("exclude", { mode: "json" }) .$type() .notNull() .default(sql`'[]'`), scheduleConfig: text("schedule_config", { mode: "json" }) .$type>() .notNull(), cleanupConfig: text("cleanup_config", { mode: "json" }) .$type>() .notNull(), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (_table) => []); export const repositories = sqliteTable("repositories", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), configId: text("config_id") .notNull() .references(() => configs.id), name: text("name").notNull(), fullName: text("full_name").notNull(), normalizedFullName: text("normalized_full_name").notNull(), url: text("url").notNull(), cloneUrl: text("clone_url").notNull(), owner: text("owner").notNull(), organization: text("organization"), mirroredLocation: text("mirrored_location").default(""), isPrivate: integer("is_private", { mode: "boolean" }) .notNull() .default(false), isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false), forkedFrom: text("forked_from"), hasIssues: integer("has_issues", { mode: "boolean" }) .notNull() .default(false), isStarred: integer("is_starred", { mode: "boolean" }) .notNull() .default(false), isArchived: integer("is_archived", { mode: "boolean" }) .notNull() .default(false), size: integer("size").notNull().default(0), hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false), hasSubmodules: integer("has_submodules", { mode: "boolean" }) .notNull() .default(false), language: text("language"), description: text("description"), defaultBranch: text("default_branch").notNull(), visibility: text("visibility").notNull().default("public"), status: text("status").notNull().default("imported"), lastMirrored: integer("last_mirrored", { mode: "timestamp" }), errorMessage: text("error_message"), destinationOrg: text("destination_org"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_repositories_user_id").on(table.userId), index("idx_repositories_config_id").on(table.configId), index("idx_repositories_status").on(table.status), index("idx_repositories_owner").on(table.owner), index("idx_repositories_organization").on(table.organization), index("idx_repositories_is_fork").on(table.isForked), index("idx_repositories_is_starred").on(table.isStarred), uniqueIndex("uniq_repositories_user_full_name").on(table.userId, table.fullName), uniqueIndex("uniq_repositories_user_normalized_full_name").on(table.userId, table.normalizedFullName), ]); export const mirrorJobs = sqliteTable("mirror_jobs", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), repositoryId: text("repository_id"), repositoryName: text("repository_name"), organizationId: text("organization_id"), organizationName: text("organization_name"), details: text("details"), status: text("status").notNull().default("imported"), message: text("message").notNull(), timestamp: integer("timestamp", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), // Job resilience fields jobType: text("job_type").notNull().default("mirror"), batchId: text("batch_id"), totalItems: integer("total_items"), completedItems: integer("completed_items").default(0), itemIds: text("item_ids", { mode: "json" }).$type(), completedItemIds: text("completed_item_ids", { mode: "json" }) .$type() .default(sql`'[]'`), inProgress: integer("in_progress", { mode: "boolean" }) .notNull() .default(false), startedAt: integer("started_at", { mode: "timestamp" }), completedAt: integer("completed_at", { mode: "timestamp" }), lastCheckpoint: integer("last_checkpoint", { mode: "timestamp" }), }, (table) => [ index("idx_mirror_jobs_user_id").on(table.userId), index("idx_mirror_jobs_batch_id").on(table.batchId), index("idx_mirror_jobs_in_progress").on(table.inProgress), index("idx_mirror_jobs_job_type").on(table.jobType), index("idx_mirror_jobs_timestamp").on(table.timestamp), ]); export const organizations = sqliteTable("organizations", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), configId: text("config_id") .notNull() .references(() => configs.id), name: text("name").notNull(), normalizedName: text("normalized_name").notNull(), avatarUrl: text("avatar_url").notNull(), membershipRole: text("membership_role").notNull().default("member"), isIncluded: integer("is_included", { mode: "boolean" }) .notNull() .default(true), destinationOrg: text("destination_org"), status: text("status").notNull().default("imported"), lastMirrored: integer("last_mirrored", { mode: "timestamp" }), errorMessage: text("error_message"), repositoryCount: integer("repository_count").notNull().default(0), publicRepositoryCount: integer("public_repository_count"), privateRepositoryCount: integer("private_repository_count"), forkRepositoryCount: integer("fork_repository_count"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_organizations_user_id").on(table.userId), index("idx_organizations_config_id").on(table.configId), index("idx_organizations_status").on(table.status), index("idx_organizations_is_included").on(table.isIncluded), uniqueIndex("uniq_organizations_user_normalized_name").on(table.userId, table.normalizedName), ]); // ===== Better Auth Tables ===== // Sessions table export const sessions = sqliteTable("sessions", { id: text("id").primaryKey(), token: text("token").notNull().unique(), userId: text("user_id").notNull().references(() => users.id), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_sessions_user_id").on(table.userId), index("idx_sessions_token").on(table.token), index("idx_sessions_expires_at").on(table.expiresAt), ]); // Accounts table (for OAuth providers and credentials) export const accounts = sqliteTable("accounts", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), userId: text("user_id").notNull().references(() => users.id), providerId: text("provider_id").notNull(), providerUserId: text("provider_user_id"), // Make nullable for email/password auth accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }), refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }), scope: text("scope"), expiresAt: integer("expires_at", { mode: "timestamp" }), password: text("password"), // For credential provider createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_accounts_account_id").on(table.accountId), index("idx_accounts_user_id").on(table.userId), index("idx_accounts_provider").on(table.providerId, table.providerUserId), ]); // Verification tokens table export const verificationTokens = sqliteTable("verification_tokens", { id: text("id").primaryKey(), token: text("token").notNull().unique(), identifier: text("identifier").notNull(), type: text("type").notNull(), // email, password-reset, etc expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_verification_tokens_token").on(table.token), index("idx_verification_tokens_identifier").on(table.identifier), ]); // Verifications table (for Better Auth) export const verifications = sqliteTable("verifications", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_verifications_identifier").on(table.identifier), ]); // ===== OIDC Provider Tables ===== // OAuth Applications table export const oauthApplications = sqliteTable("oauth_applications", { id: text("id").primaryKey(), clientId: text("client_id").notNull().unique(), clientSecret: text("client_secret").notNull(), name: text("name").notNull(), redirectURLs: text("redirect_urls").notNull(), // Comma-separated list metadata: text("metadata"), // JSON string type: text("type").notNull(), // web, mobile, etc disabled: integer("disabled", { mode: "boolean" }).notNull().default(false), userId: text("user_id"), // Optional - owner of the application createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_oauth_applications_client_id").on(table.clientId), index("idx_oauth_applications_user_id").on(table.userId), ]); // OAuth Access Tokens table export const oauthAccessTokens = sqliteTable("oauth_access_tokens", { id: text("id").primaryKey(), accessToken: text("access_token").notNull(), refreshToken: text("refresh_token"), accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }).notNull(), refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }), clientId: text("client_id").notNull(), userId: text("user_id").notNull().references(() => users.id), scopes: text("scopes").notNull(), // Comma-separated list createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_oauth_access_tokens_access_token").on(table.accessToken), index("idx_oauth_access_tokens_user_id").on(table.userId), index("idx_oauth_access_tokens_client_id").on(table.clientId), ]); // OAuth Consent table export const oauthConsent = sqliteTable("oauth_consent", { id: text("id").primaryKey(), userId: text("user_id").notNull().references(() => users.id), clientId: text("client_id").notNull(), scopes: text("scopes").notNull(), // Comma-separated list consentGiven: integer("consent_given", { mode: "boolean" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_oauth_consent_user_id").on(table.userId), index("idx_oauth_consent_client_id").on(table.clientId), index("idx_oauth_consent_user_client").on(table.userId, table.clientId), ]); // ===== SSO Provider Tables ===== // SSO Providers table export const ssoProviders = sqliteTable("sso_providers", { id: text("id").primaryKey(), issuer: text("issuer").notNull(), domain: text("domain").notNull(), oidcConfig: text("oidc_config").notNull(), // JSON string with OIDC configuration userId: text("user_id").notNull(), // Admin who created this provider providerId: text("provider_id").notNull().unique(), // Unique identifier for the provider organizationId: text("organization_id"), // Optional - if provider is linked to an organization createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_sso_providers_provider_id").on(table.providerId), index("idx_sso_providers_domain").on(table.domain), index("idx_sso_providers_issuer").on(table.issuer), ]); // ===== Rate Limit Tracking ===== export const rateLimitSchema = z.object({ id: z.string(), userId: z.string(), provider: z.enum(["github", "gitea"]).default("github"), limit: z.number(), remaining: z.number(), used: z.number(), reset: z.coerce.date(), retryAfter: z.number().optional(), // seconds to wait status: z.enum(["ok", "warning", "limited", "exceeded"]).default("ok"), lastChecked: z.coerce.date(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); export const rateLimits = sqliteTable("rate_limits", { id: text("id").primaryKey(), userId: text("user_id") .notNull() .references(() => users.id), provider: text("provider").notNull().default("github"), limit: integer("limit").notNull(), remaining: integer("remaining").notNull(), used: integer("used").notNull(), reset: integer("reset", { mode: "timestamp" }).notNull(), retryAfter: integer("retry_after"), // seconds to wait status: text("status").notNull().default("ok"), lastChecked: integer("last_checked", { mode: "timestamp" }).notNull(), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .default(sql`(unixepoch())`), }, (table) => [ index("idx_rate_limits_user_provider").on(table.userId, table.provider), index("idx_rate_limits_status").on(table.status), ]); // Export type definitions export type User = z.infer; export type Config = z.infer; export type Repository = z.infer; export type MirrorJob = z.infer; export type Organization = z.infer; export type Event = z.infer; export type RateLimit = z.infer;