TypeScript 97.5%
SQL 1.4%
Python 0.8%
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