Files
gitea-mirror/src/lib/db/schema.ts
Arunavo Ray d59a07a8c5 fix: add metadata field to repositories table to prevent duplicate issues on sync
Fixes #141

The repository metadata field was missing from the database schema, which
caused the metadata sync state (issues, PRs, releases, etc.) to not persist.
This resulted in duplicate issues being created every time a repository was
synced because the system couldn't track what had already been mirrored.

Changes:
- Added metadata text field to repositories table in schema
- Added metadata field to repositorySchema Zod validation
- Generated database migration 0008_serious_thena.sql

Root cause analysis:
1. Code tried to read/write repository.metadata to track mirrored components
2. The metadata field didn't exist in the database schema
3. On sync, metadataState.components.issues was always false
4. This triggered re-mirroring of all issues, creating duplicates

The fix ensures metadata state persists between mirrors and syncs, preventing
duplicate metadata (issues, PRs, releases) from being created in Gitea.
2025-10-30 10:58:48 +05:30

700 lines
25 KiB
TypeScript

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(),
metadata: z.string().optional().nullable(), // JSON string for metadata sync state
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<z.infer<typeof githubConfigSchema>>()
.notNull(),
giteaConfig: text("gitea_config", { mode: "json" })
.$type<z.infer<typeof giteaConfigSchema>>()
.notNull(),
include: text("include", { mode: "json" })
.$type<string[]>()
.notNull()
.default(sql`'["*"]'`),
exclude: text("exclude", { mode: "json" })
.$type<string[]>()
.notNull()
.default(sql`'[]'`),
scheduleConfig: text("schedule_config", { mode: "json" })
.$type<z.infer<typeof scheduleConfigSchema>>()
.notNull(),
cleanupConfig: text("cleanup_config", { mode: "json" })
.$type<z.infer<typeof cleanupConfigSchema>>()
.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"),
metadata: text("metadata"), // JSON string storing metadata sync state (issues, PRs, releases, etc.)
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<string[]>(),
completedItemIds: text("completed_item_ids", { mode: "json" })
.$type<string[]>()
.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<typeof userSchema>;
export type Config = z.infer<typeof configSchema>;
export type Repository = z.infer<typeof repositorySchema>;
export type MirrorJob = z.infer<typeof mirrorJobSchema>;
export type Organization = z.infer<typeof organizationSchema>;
export type Event = z.infer<typeof eventSchema>;
export type RateLimit = z.infer<typeof rateLimitSchema>;