SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
27.5 KB · 658 lines typescript
Raw Blame History
1import {2  pgTable,3  text,4  timestamp,5  boolean,6  integer,7  bigint,8  doublePrecision,9  jsonb,10  index,11  uniqueIndex,12  primaryKey,13} from "drizzle-orm/pg-core";14import { relations, sql } from "drizzle-orm";1516const ts = (name: string) => timestamp(name, { withTimezone: true, mode: "date" });17const now = () => sql`now()`;1819// ---------------------------------------------------------------------------20// Auth (Better Auth compatible)21// ---------------------------------------------------------------------------22export const users = pgTable(23  "users",24  {25    id: text("id").primaryKey(),26    name: text("name").notNull().default(""),27    email: text("email").notNull(),28    emailVerified: boolean("email_verified").notNull().default(false),29    image: text("image"),30    role: text("role").notNull().default("user"), // user | admin31    banned: boolean("banned").notNull().default(false),32    banReason: text("ban_reason"),33    onboardingCompletedAt: ts("onboarding_completed_at"),34    createdAt: ts("created_at").notNull().default(now()),35    updatedAt: ts("updated_at").notNull().default(now()),36  },37  (t) => [uniqueIndex("users_email_uq").on(t.email)],38);3940export const sessions = pgTable(41  "sessions",42  {43    id: text("id").primaryKey(),44    expiresAt: ts("expires_at").notNull(),45    token: text("token").notNull(),46    createdAt: ts("created_at").notNull().default(now()),47    updatedAt: ts("updated_at").notNull().default(now()),48    ipAddress: text("ip_address"),49    userAgent: text("user_agent"),50    userId: text("user_id")51      .notNull()52      .references(() => users.id, { onDelete: "cascade" }),53  },54  (t) => [uniqueIndex("sessions_token_uq").on(t.token), index("sessions_user_idx").on(t.userId)],55);5657export const accounts = pgTable(58  "accounts",59  {60    id: text("id").primaryKey(),61    accountId: text("account_id").notNull(),62    providerId: text("provider_id").notNull(),63    userId: text("user_id")64      .notNull()65      .references(() => users.id, { onDelete: "cascade" }),66    accessToken: text("access_token"),67    refreshToken: text("refresh_token"),68    idToken: text("id_token"),69    accessTokenExpiresAt: ts("access_token_expires_at"),70    refreshTokenExpiresAt: ts("refresh_token_expires_at"),71    scope: text("scope"),72    password: text("password"),73    createdAt: ts("created_at").notNull().default(now()),74    updatedAt: ts("updated_at").notNull().default(now()),75  },76  (t) => [index("accounts_user_idx").on(t.userId)],77);7879export const verifications = pgTable(80  "verifications",81  {82    id: text("id").primaryKey(),83    identifier: text("identifier").notNull(),84    value: text("value").notNull(),85    expiresAt: ts("expires_at").notNull(),86    createdAt: ts("created_at").notNull().default(now()),87    updatedAt: ts("updated_at").notNull().default(now()),88  },89  (t) => [index("verifications_identifier_idx").on(t.identifier)],90);9192/**93 * Signup allowlist: Fetcha is invitation-only. An administrator adds an email here (optionally94 * sending an invitation), and only then can that address create an account.95 */96export const signupAllowlist = pgTable(97  "signup_allowlist",98  {99    email: text("email").primaryKey(), // lower-cased100    note: text("note"),101    invitedByUserId: text("invited_by_user_id").references(() => users.id, { onDelete: "set null" }),102    /** Last time an invitation email was sent. */103    invitedAt: ts("invited_at"),104    /** Set when the account was created. */105    usedAt: ts("used_at"),106    userId: text("user_id").references(() => users.id, { onDelete: "set null" }),107    createdAt: ts("created_at").notNull().default(now()),108  },109  (t) => [index("signup_allowlist_user_idx").on(t.userId)],110);111112// ---------------------------------------------------------------------------113// Organizations & projects114// ---------------------------------------------------------------------------115export const organizations = pgTable(116  "organizations",117  {118    id: text("id").primaryKey(),119    name: text("name").notNull(),120    slug: text("slug").notNull(),121    plan: text("plan").notNull().default("unlimited"),122    ownerUserId: text("owner_user_id")123      .notNull()124      .references(() => users.id, { onDelete: "restrict" }),125    /** Show upstream provider names in debug output (admin-enabled). */126    providerVisibility: boolean("provider_visibility").notNull().default(false),127    softLimitUsd: doublePrecision("soft_limit_usd"),128    hardLimitUsd: doublePrecision("hard_limit_usd"),129    stripeCustomerId: text("stripe_customer_id"),130    suspended: boolean("suspended").notNull().default(false),131    createdAt: ts("created_at").notNull().default(now()),132    updatedAt: ts("updated_at").notNull().default(now()),133  },134  (t) => [uniqueIndex("organizations_slug_uq").on(t.slug), index("organizations_owner_idx").on(t.ownerUserId)],135);136137export const organizationMembers = pgTable(138  "organization_members",139  {140    organizationId: text("organization_id")141      .notNull()142      .references(() => organizations.id, { onDelete: "cascade" }),143    userId: text("user_id")144      .notNull()145      .references(() => users.id, { onDelete: "cascade" }),146    role: text("role").notNull().default("owner"), // owner | admin | developer | billing | viewer147    createdAt: ts("created_at").notNull().default(now()),148  },149  (t) => [primaryKey({ columns: [t.organizationId, t.userId] }), index("org_members_user_idx").on(t.userId)],150);151152export const projects = pgTable(153  "projects",154  {155    id: text("id").primaryKey(),156    organizationId: text("organization_id")157      .notNull()158      .references(() => organizations.id, { onDelete: "cascade" }),159    name: text("name").notNull(),160    slug: text("slug").notNull(),161    description: text("description"),162    environment: text("environment").notNull().default("production"), // production | development163    defaultCountry: text("default_country"),164    defaultNetwork: text("default_network").notNull().default("auto"),165    logLevel: text("log_level").notNull().default("metadata"), // none | metadata | headers | full166    softLimitUsd: doublePrecision("soft_limit_usd"),167    hardLimitUsd: doublePrecision("hard_limit_usd"),168    monthlyRequestLimit: integer("monthly_request_limit"),169    archivedAt: ts("archived_at"),170    createdAt: ts("created_at").notNull().default(now()),171    updatedAt: ts("updated_at").notNull().default(now()),172  },173  (t) => [uniqueIndex("projects_org_slug_uq").on(t.organizationId, t.slug), index("projects_org_idx").on(t.organizationId)],174);175176export const apiKeys = pgTable(177  "api_keys",178  {179    id: text("id").primaryKey(),180    organizationId: text("organization_id")181      .notNull()182      .references(() => organizations.id, { onDelete: "cascade" }),183    projectId: text("project_id")184      .notNull()185      .references(() => projects.id, { onDelete: "cascade" }),186    createdByUserId: text("created_by_user_id").references(() => users.id, { onDelete: "set null" }),187    name: text("name").notNull(),188    keyHash: text("key_hash").notNull(),189    keyPrefix: text("key_prefix").notNull(),190    last4: text("last4").notNull(),191    mode: text("mode").notNull().default("live"), // live | test192    scopes: jsonb("scopes").$type<string[]>().notNull().default(sql`'["fetch:execute","sessions:write","usage:read"]'::jsonb`),193    expiresAt: ts("expires_at"),194    lastUsedAt: ts("last_used_at"),195    revokedAt: ts("revoked_at"),196    rotatedFromId: text("rotated_from_id"),197    createdAt: ts("created_at").notNull().default(now()),198  },199  (t) => [uniqueIndex("api_keys_hash_uq").on(t.keyHash), index("api_keys_project_idx").on(t.projectId), index("api_keys_org_idx").on(t.organizationId)],200);201202// ---------------------------------------------------------------------------203// Requests204// ---------------------------------------------------------------------------205export const fetchRequests = pgTable(206  "fetch_requests",207  {208    id: text("id").primaryKey(), // req_xxx209    organizationId: text("organization_id")210      .notNull()211      .references(() => organizations.id, { onDelete: "cascade" }),212    projectId: text("project_id")213      .notNull()214      .references(() => projects.id, { onDelete: "cascade" }),215    apiKeyId: text("api_key_id").references(() => apiKeys.id, { onDelete: "set null" }),216    source: text("source").notNull().default("api"), // api | playground | sdk217    url: text("url").notNull(),218    finalUrl: text("final_url"),219    domain: text("domain").notNull(),220    method: text("method").notNull().default("GET"),221    requestedNetwork: text("requested_network").notNull().default("auto"),222    network: text("network"), // resolved concrete network223    country: text("country"),224    region: text("region"),225    city: text("city"),226    sessionId: text("session_id"),227    browser: boolean("browser").notNull().default(false),228    /** Execution mode of the final attempt: http | browser. */229    mode: text("mode"),230    format: text("format").notNull().default("html"),231    status: text("status").notNull().default("pending"), // pending | success | failed232    httpStatus: integer("http_status"),233    errorCode: text("error_code"),234    errorMessage: text("error_message"),235    attempts: integer("attempts").notNull().default(0),236    latencyMs: integer("latency_ms"),237    bytesIn: bigint("bytes_in", { mode: "number" }).notNull().default(0),238    bytesOut: bigint("bytes_out", { mode: "number" }).notNull().default(0),239    costUsd: doublePrecision("cost_usd").notNull().default(0), // upstream cost240    priceUsd: doublePrecision("price_usd").notNull().default(0), // billed to customer241    cached: boolean("cached").notNull().default(false),242    requestHeaders: jsonb("request_headers").$type<Record<string, string>>(),243    responseHeaders: jsonb("response_headers").$type<Record<string, string>>(),244    timing: jsonb("timing").$type<Record<string, number>>(),245    clientIp: text("client_ip"),246    userAgent: text("user_agent"),247    createdAt: ts("created_at").notNull().default(now()),248    completedAt: ts("completed_at"),249  },250  (t) => [251    index("fetch_requests_project_created_idx").on(t.projectId, t.createdAt),252    index("fetch_requests_org_created_idx").on(t.organizationId, t.createdAt),253    index("fetch_requests_domain_idx").on(t.domain),254    index("fetch_requests_created_idx").on(t.createdAt),255  ],256);257258export const requestAttempts = pgTable(259  "request_attempts",260  {261    id: text("id").primaryKey(), // att_xxx262    requestId: text("request_id")263      .notNull()264      .references(() => fetchRequests.id, { onDelete: "cascade" }),265    attemptNo: integer("attempt_no").notNull(),266    provider: text("provider").notNull(), // oxylabs | decodo | soax | direct267    network: text("network").notNull(),268    mode: text("mode").notNull().default("http"), // http | browser269    country: text("country"),270    sessionKey: text("session_key"),271    outcome: text("outcome").notNull(), // success | blocked | timeout | error | provider_error272    httpStatus: integer("http_status"),273    errorCode: text("error_code"),274    errorDetail: text("error_detail"), // internal only, may contain provider text275    blockReason: text("block_reason"),276    durationMs: integer("duration_ms").notNull().default(0),277    bytesIn: bigint("bytes_in", { mode: "number" }).notNull().default(0),278    bytesOut: bigint("bytes_out", { mode: "number" }).notNull().default(0),279    unitPricePerGb: doublePrecision("unit_price_per_gb").notNull().default(0),280    costUsd: doublePrecision("cost_usd").notNull().default(0),281    routingScore: doublePrecision("routing_score"),282    timing: jsonb("timing").$type<Record<string, number>>(),283    createdAt: ts("created_at").notNull().default(now()),284  },285  (t) => [index("request_attempts_request_idx").on(t.requestId), index("request_attempts_provider_created_idx").on(t.provider, t.createdAt)],286);287288export const proxySessions = pgTable(289  "proxy_sessions",290  {291    id: text("id").primaryKey(), // sess_xxx292    organizationId: text("organization_id")293      .notNull()294      .references(() => organizations.id, { onDelete: "cascade" }),295    projectId: text("project_id")296      .notNull()297      .references(() => projects.id, { onDelete: "cascade" }),298    label: text("label"),299    provider: text("provider").notNull(),300    network: text("network").notNull(),301    country: text("country"),302    region: text("region"),303    city: text("city"),304    /** Provider-side sticky key; internal only. */305    stickyKey: text("sticky_key").notNull(),306    cookies: jsonb("cookies").$type<Array<{ name: string; value: string; domain?: string; path?: string }>>().notNull().default(sql`'[]'::jsonb`),307    status: text("status").notNull().default("active"), // active | expired | closed308    requestCount: integer("request_count").notNull().default(0),309    lastUsedAt: ts("last_used_at"),310    expiresAt: ts("expires_at").notNull(),311    createdAt: ts("created_at").notNull().default(now()),312  },313  (t) => [index("proxy_sessions_project_idx").on(t.projectId), index("proxy_sessions_expires_idx").on(t.expiresAt)],314);315316// ---------------------------------------------------------------------------317// Usage & billing318// ---------------------------------------------------------------------------319export const usageEvents = pgTable(320  "usage_events",321  {322    id: text("id").primaryKey(),323    organizationId: text("organization_id")324      .notNull()325      .references(() => organizations.id, { onDelete: "cascade" }),326    projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),327    requestId: text("request_id"),328    metric: text("metric").notNull(), // request | bandwidth | residential_bandwidth | mobile_bandwidth | browser_seconds | extraction329    quantity: doublePrecision("quantity").notNull(),330    unit: text("unit").notNull(), // count | bytes | seconds331    costUsd: doublePrecision("cost_usd").notNull().default(0), // customer price332    upstreamCostUsd: doublePrecision("upstream_cost_usd").notNull().default(0),333    createdAt: ts("created_at").notNull().default(now()),334  },335  (t) => [index("usage_events_org_created_idx").on(t.organizationId, t.createdAt), index("usage_events_project_created_idx").on(t.projectId, t.createdAt)],336);337338export const subscriptions = pgTable("subscriptions", {339  id: text("id").primaryKey(),340  organizationId: text("organization_id")341    .notNull()342    .references(() => organizations.id, { onDelete: "cascade" }),343  plan: text("plan").notNull(),344  status: text("status").notNull().default("active"), // active | trialing | past_due | canceled345  stripeSubscriptionId: text("stripe_subscription_id"),346  currentPeriodStart: ts("current_period_start"),347  currentPeriodEnd: ts("current_period_end"),348  cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull().default(false),349  createdAt: ts("created_at").notNull().default(now()),350  updatedAt: ts("updated_at").notNull().default(now()),351});352353export const billingEvents = pgTable(354  "billing_events",355  {356    id: text("id").primaryKey(),357    organizationId: text("organization_id").references(() => organizations.id, { onDelete: "set null" }),358    type: text("type").notNull(),359    stripeEventId: text("stripe_event_id"),360    payload: jsonb("payload").$type<Record<string, unknown>>(),361    createdAt: ts("created_at").notNull().default(now()),362  },363  (t) => [index("billing_events_org_idx").on(t.organizationId)],364);365366// ---------------------------------------------------------------------------367// Crawl jobs368// ---------------------------------------------------------------------------369export const crawlJobs = pgTable(370  "crawl_jobs",371  {372    id: text("id").primaryKey(), // crawl_xxx373    organizationId: text("organization_id")374      .notNull()375      .references(() => organizations.id, { onDelete: "cascade" }),376    projectId: text("project_id")377      .notNull()378      .references(() => projects.id, { onDelete: "cascade" }),379    apiKeyId: text("api_key_id").references(() => apiKeys.id, { onDelete: "set null" }),380    source: text("source").notNull().default("api"), // api | playground | sdk381    label: text("label"),382    seedUrl: text("seed_url").notNull(),383    domain: text("domain").notNull(),384    options: jsonb("options").$type<Record<string, unknown>>().notNull(),385    status: text("status").notNull().default("queued"), // queued | running | completed | failed | cancelled386    workerId: text("worker_id"),387    pagesDiscovered: integer("pages_discovered").notNull().default(0),388    pagesFetched: integer("pages_fetched").notNull().default(0),389    pagesOk: integer("pages_ok").notNull().default(0),390    pagesBlocked: integer("pages_blocked").notNull().default(0),391    pagesFailed: integer("pages_failed").notNull().default(0),392    bytes: bigint("bytes", { mode: "number" }).notNull().default(0),393    costUsd: doublePrecision("cost_usd").notNull().default(0),394    errorCode: text("error_code"),395    errorMessage: text("error_message"),396    webhookStatus: text("webhook_status"),397    createdAt: ts("created_at").notNull().default(now()),398    startedAt: ts("started_at"),399    heartbeatAt: ts("heartbeat_at"),400    completedAt: ts("completed_at"),401  },402  (t) => [index("crawl_jobs_project_created_idx").on(t.projectId, t.createdAt), index("crawl_jobs_status_idx").on(t.status)],403);404405export const crawlPages = pgTable(406  "crawl_pages",407  {408    id: text("id").primaryKey(), // cpg_xxx409    jobId: text("job_id")410      .notNull()411      .references(() => crawlJobs.id, { onDelete: "cascade" }),412    url: text("url").notNull(),413    finalUrl: text("final_url"),414    depth: integer("depth").notNull().default(0),415    parentUrl: text("parent_url"),416    status: text("status").notNull().default("pending"), // pending | success | blocked | failed | skipped417    httpStatus: integer("http_status"),418    errorCode: text("error_code"),419    requestId: text("request_id"),420    title: text("title"),421    description: text("description"),422    contentType: text("content_type"),423    /** Converted content (markdown / text / html) — bounded by the job's per-page cap. */424    content: text("content"),425    linksCount: integer("links_count").notNull().default(0),426    bytes: bigint("bytes", { mode: "number" }).notNull().default(0),427    durationMs: integer("duration_ms"),428    mode: text("mode"), // http | browser429    createdAt: ts("created_at").notNull().default(now()),430    fetchedAt: ts("fetched_at"),431  },432  (t) => [index("crawl_pages_job_idx").on(t.jobId, t.createdAt), uniqueIndex("crawl_pages_job_url_uq").on(t.jobId, t.url)],433);434435// ---------------------------------------------------------------------------436// Providers, routing intelligence437// ---------------------------------------------------------------------------438export const providerConfigs = pgTable("provider_configs", {439  id: text("id").primaryKey(), // oxylabs | decodo | soax440  label: text("label").notNull(),441  enabled: boolean("enabled").notNull().default(true),442  networks: jsonb("networks").$type<string[]>().notNull().default(sql`'["residential"]'::jsonb`),443  pricePerGbUsd: jsonb("price_per_gb_usd").$type<Record<string, number>>().notNull().default(sql`'{}'::jsonb`),444  weight: doublePrecision("weight").notNull().default(1),445  maxConcurrency: integer("max_concurrency").notNull().default(200),446  notes: text("notes"),447  updatedAt: ts("updated_at").notNull().default(now()),448});449450export const providerHealth = pgTable(451  "provider_health",452  {453    id: text("id").primaryKey(),454    provider: text("provider").notNull(),455    network: text("network").notNull(),456    status: text("status").notNull(), // healthy | degraded | down | unconfigured457    latencyMs: integer("latency_ms"),458    successRate: doublePrecision("success_rate"),459    detail: text("detail"),460    checkedAt: ts("checked_at").notNull().default(now()),461  },462  (t) => [index("provider_health_provider_checked_idx").on(t.provider, t.checkedAt)],463);464465export const domainProfiles = pgTable(466  "domain_profiles",467  {468    domain: text("domain").primaryKey(),469    preferredNetwork: text("preferred_network"),470    preferredProvider: text("preferred_provider"),471    requests: integer("requests").notNull().default(0),472    successes: integer("successes").notNull().default(0),473    blocks: integer("blocks").notNull().default(0),474    captchas: integer("captchas").notNull().default(0),475    browserRequired: integer("browser_required").notNull().default(0),476    avgLatencyMs: doublePrecision("avg_latency_ms").notNull().default(0),477    /** { "oxylabs:residential": { n, ok, blocked, latency_ms_avg, cost_usd } } */478    routeStats: jsonb("route_stats").$type<Record<string, { n: number; ok: number; blocked: number; lat: number; cost: number }>>().notNull().default(sql`'{}'::jsonb`),479    policy: jsonb("policy").$type<{ order?: string[]; force_network?: string; browser?: boolean }>(),480    lastSeenAt: ts("last_seen_at").notNull().default(now()),481    updatedAt: ts("updated_at").notNull().default(now()),482  },483);484485export const routingMetrics = pgTable(486  "routing_metrics",487  {488    id: text("id").primaryKey(),489    bucket: ts("bucket").notNull(), // hourly bucket490    provider: text("provider").notNull(),491    network: text("network").notNull(),492    country: text("country"),493    requests: integer("requests").notNull().default(0),494    successes: integer("successes").notNull().default(0),495    blocked: integer("blocked").notNull().default(0),496    errors: integer("errors").notNull().default(0),497    latencySumMs: bigint("latency_sum_ms", { mode: "number" }).notNull().default(0),498    bytes: bigint("bytes", { mode: "number" }).notNull().default(0),499    costUsd: doublePrecision("cost_usd").notNull().default(0),500  },501  (t) => [uniqueIndex("routing_metrics_bucket_uq").on(t.bucket, t.provider, t.network, t.country)],502);503504// ---------------------------------------------------------------------------505// Webhooks, audit, abuse, flags, legal506// ---------------------------------------------------------------------------507export const webhooks = pgTable("webhooks", {508  id: text("id").primaryKey(),509  organizationId: text("organization_id")510    .notNull()511    .references(() => organizations.id, { onDelete: "cascade" }),512  projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),513  url: text("url").notNull(),514  secret: text("secret").notNull(),515  events: jsonb("events").$type<string[]>().notNull().default(sql`'[]'::jsonb`),516  enabled: boolean("enabled").notNull().default(true),517  createdAt: ts("created_at").notNull().default(now()),518});519520export const webhookDeliveries = pgTable(521  "webhook_deliveries",522  {523    id: text("id").primaryKey(),524    webhookId: text("webhook_id")525      .notNull()526      .references(() => webhooks.id, { onDelete: "cascade" }),527    event: text("event").notNull(),528    payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),529    status: text("status").notNull().default("pending"),530    responseStatus: integer("response_status"),531    attempts: integer("attempts").notNull().default(0),532    nextAttemptAt: ts("next_attempt_at"),533    createdAt: ts("created_at").notNull().default(now()),534  },535  (t) => [index("webhook_deliveries_webhook_idx").on(t.webhookId)],536);537538export const auditLogs = pgTable(539  "audit_logs",540  {541    id: text("id").primaryKey(),542    organizationId: text("organization_id").references(() => organizations.id, { onDelete: "cascade" }),543    userId: text("user_id").references(() => users.id, { onDelete: "set null" }),544    action: text("action").notNull(), // login | password.changed | api_key.created | ...545    target: text("target"),546    metadata: jsonb("metadata").$type<Record<string, unknown>>(),547    ipAddress: text("ip_address"),548    userAgent: text("user_agent"),549    createdAt: ts("created_at").notNull().default(now()),550  },551  (t) => [index("audit_logs_org_created_idx").on(t.organizationId, t.createdAt), index("audit_logs_user_idx").on(t.userId)],552);553554export const abuseEvents = pgTable(555  "abuse_events",556  {557    id: text("id").primaryKey(),558    organizationId: text("organization_id").references(() => organizations.id, { onDelete: "cascade" }),559    projectId: text("project_id"),560    requestId: text("request_id"),561    kind: text("kind").notNull(), // ssrf_attempt | prohibited_target | rate_abuse | manual562    severity: text("severity").notNull().default("low"),563    detail: text("detail"),564    resolved: boolean("resolved").notNull().default(false),565    createdAt: ts("created_at").notNull().default(now()),566  },567  (t) => [index("abuse_events_org_idx").on(t.organizationId)],568);569570export const featureFlags = pgTable("feature_flags", {571  key: text("key").primaryKey(),572  enabled: boolean("enabled").notNull().default(false),573  description: text("description"),574  plans: jsonb("plans").$type<string[]>(),575  organizationIds: jsonb("organization_ids").$type<string[]>(),576  updatedAt: ts("updated_at").notNull().default(now()),577});578579export const legalAcceptances = pgTable(580  "legal_acceptances",581  {582    id: text("id").primaryKey(),583    userId: text("user_id")584      .notNull()585      .references(() => users.id, { onDelete: "cascade" }),586    document: text("document").notNull(), // terms | privacy | acceptable_use587    version: text("version").notNull(),588    ipAddress: text("ip_address"),589    acceptedAt: ts("accepted_at").notNull().default(now()),590  },591  (t) => [index("legal_acceptances_user_idx").on(t.userId)],592);593594export const statusIncidents = pgTable("status_incidents", {595  id: text("id").primaryKey(),596  component: text("component").notNull(),597  title: text("title").notNull(),598  body: text("body"),599  severity: text("severity").notNull().default("minor"),600  startedAt: ts("started_at").notNull().default(now()),601  resolvedAt: ts("resolved_at"),602});603604// ---------------------------------------------------------------------------605// Relations606// ---------------------------------------------------------------------------607export const usersRelations = relations(users, ({ many }) => ({608  memberships: many(organizationMembers),609  ownedOrganizations: many(organizations),610}));611612export const organizationsRelations = relations(organizations, ({ many, one }) => ({613  owner: one(users, { fields: [organizations.ownerUserId], references: [users.id] }),614  members: many(organizationMembers),615  projects: many(projects),616  apiKeys: many(apiKeys),617}));618619export const organizationMembersRelations = relations(organizationMembers, ({ one }) => ({620  organization: one(organizations, { fields: [organizationMembers.organizationId], references: [organizations.id] }),621  user: one(users, { fields: [organizationMembers.userId], references: [users.id] }),622}));623624export const projectsRelations = relations(projects, ({ one, many }) => ({625  organization: one(organizations, { fields: [projects.organizationId], references: [organizations.id] }),626  apiKeys: many(apiKeys),627  requests: many(fetchRequests),628}));629630export const apiKeysRelations = relations(apiKeys, ({ one }) => ({631  project: one(projects, { fields: [apiKeys.projectId], references: [projects.id] }),632  organization: one(organizations, { fields: [apiKeys.organizationId], references: [organizations.id] }),633}));634635export const fetchRequestsRelations = relations(fetchRequests, ({ one, many }) => ({636  project: one(projects, { fields: [fetchRequests.projectId], references: [projects.id] }),637  attempts: many(requestAttempts),638}));639640export const requestAttemptsRelations = relations(requestAttempts, ({ one }) => ({641  request: one(fetchRequests, { fields: [requestAttempts.requestId], references: [fetchRequests.id] }),642}));643644export type User = typeof users.$inferSelect;645export type Organization = typeof organizations.$inferSelect;646export type Project = typeof projects.$inferSelect;647export type ApiKey = typeof apiKeys.$inferSelect;648export type FetchRequestRow = typeof fetchRequests.$inferSelect;649export type RequestAttemptRow = typeof requestAttempts.$inferSelect;650export type ProxySession = typeof proxySessions.$inferSelect;651export type UsageEvent = typeof usageEvents.$inferSelect;652export type DomainProfile = typeof domainProfiles.$inferSelect;653export type ProviderHealthRow = typeof providerHealth.$inferSelect;654export type AuditLog = typeof auditLogs.$inferSelect;655export type SignupAllowlistRow = typeof signupAllowlist.$inferSelect;656export type CrawlJob = typeof crawlJobs.$inferSelect;657export type CrawlPage = typeof crawlPages.$inferSelect;658