import { table, text, integer, now, index, ownableColumns, createSharesTable, uniqueIndex, } from "@agent-native/core/db/schema"; // Feature-owned schema modules. Re-exported so their tables join this app's // Drizzle schema namespace (schema.). Each file is owned by a single // feature so parallel work never collides on this shared file. export * from "./schema-monitoring.js"; export * from "./schema-errors.js"; /** * Dashboards table — covers both Explorer and SQL dashboards. The * distinction lives in `kind` and the shape of the `config` JSON blob. * Previously stored in the settings KV store under * `u::dashboard-{id}` / `u::sql-dashboard-{id}` / * `o::sql-dashboard-{id}`. Those keys are read as a fallback * during lazy migration (see server/lib/dashboards-store.ts) and the * legacy rows can be removed once the team is sure everyone's migrated. */ export const dashboards = table("dashboards", { id: text("id").primaryKey(), kind: text("kind", { enum: ["explorer", "sql"] }).notNull(), title: text("title").notNull().default("Untitled"), /** Full dashboard config (SqlDashboardConfig or Explorer state) as JSON. */ config: text("config").notNull(), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), /** Archive timestamp. Null = active. Archived rows are hidden from * default list responses but remain accessible by id and can be restored. */ archivedAt: text("archived_at"), /** Hidden dashboards are omitted from default navigation but remain openable. */ hiddenAt: text("hidden_at"), hiddenBy: text("hidden_by"), /** Last authenticated user who changed dashboard metadata/config, if tracked. */ updatedBy: text("updated_by"), ...ownableColumns(), }); export const dashboardShares = createSharesTable("dashboard_shares"); /** * Bounded dashboard history. Each row snapshots the previous dashboard config * before a meaningful save so users and agents can restore known-good states. */ export const dashboardRevisions = table( "dashboard_revisions", { id: text("id").primaryKey(), dashboardId: text("dashboard_id").notNull(), kind: text("kind", { enum: ["explorer", "sql"] }).notNull(), title: text("title").notNull(), config: text("config").notNull(), createdAt: text("created_at").notNull().default(now()), createdBy: text("created_by"), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }, (t) => ({ dashboardCreatedIdx: index("dashboard_revisions_dashboard_created_idx").on( t.dashboardId, t.createdAt, ), orgDashboardIdx: index("dashboard_revisions_org_dashboard_idx").on( t.orgId, t.dashboardId, ), }), ); /** * Saved filter views per dashboard. Lives alongside the parent and is * governed by the parent's sharing (no separate share rows). */ export const dashboardViews = table("dashboard_views", { id: text("id").primaryKey(), dashboardId: text("dashboard_id").notNull(), name: text("name").notNull(), /** Filter params as JSON (Record). */ filters: text("filters").notNull().default("{}"), createdBy: text("created_by"), createdAt: text("created_at").notNull().default(now()), }); /** * Scheduled email snapshots for SQL dashboards. Each row belongs to the user * who created the subscription; dashboard access is re-checked before every * send so revoking dashboard access also stops future deliveries. */ export const dashboardReportSubscriptions = table( "dashboard_report_subscriptions", { id: text("id").primaryKey(), dashboardId: text("dashboard_id").notNull(), name: text("name").notNull(), recipients: text("recipients").notNull().default("[]"), filters: text("filters").notNull().default("{}"), frequency: text("frequency", { enum: ["daily"] }) .notNull() .default("daily"), timeOfDay: text("time_of_day").notNull().default("09:00"), timezone: text("timezone").notNull().default("UTC"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), nextRunAt: text("next_run_at"), lastRunAt: text("last_run_at"), lastStatus: text("last_status", { enum: ["success", "error", "running"], }), lastError: text("last_error"), lastCaptureAt: text("last_capture_at"), lastCaptureMode: text("last_capture_mode", { enum: ["full", "partial", "none"], }), lastCaptureError: text("last_capture_error"), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }, ); /** * Ad-hoc analyses. Previously stored in the settings KV store under * `adhoc-analysis-{id}`. Those keys are read as a fallback during lazy * migration. See server/lib/analyses-store.ts. */ export const analyses = table("analyses", { id: text("id").primaryKey(), name: text("name").notNull(), description: text("description").notNull().default(""), /** Original user question that triggered the analysis. */ question: text("question").notNull().default(""), /** Step-by-step re-run instructions. */ instructions: text("instructions").notNull().default(""), /** Data sources referenced, as JSON array of strings. */ dataSources: text("data_sources").notNull().default("[]"), /** Full findings in Markdown. */ resultMarkdown: text("result_markdown").notNull().default(""), /** Optional structured result data, as JSON. */ resultData: text("result_data"), author: text("author"), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), /** Hidden analyses are omitted from default navigation but remain openable. */ hiddenAt: text("hidden_at"), hiddenBy: text("hidden_by"), ...ownableColumns(), }); export const analysisRevisions = table( "analysis_revisions", { id: text("id").primaryKey(), analysisId: text("analysis_id").notNull(), name: text("name").notNull(), description: text("description").notNull().default(""), question: text("question").notNull().default(""), instructions: text("instructions").notNull().default(""), dataSources: text("data_sources").notNull().default("[]"), resultMarkdown: text("result_markdown").notNull().default(""), resultData: text("result_data"), createdAt: text("created_at").notNull().default(now()), createdBy: text("created_by"), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }, (t) => ({ analysisCreatedIdx: index("analysis_revisions_analysis_created_idx").on( t.analysisId, t.createdAt, ), }), ); export const analysisShares = createSharesTable("analysis_shares"); /** * BigQuery result cache (pre-existing — moved here from db plugin so a * single drizzle schema covers the template). */ export const bigqueryCache = table("bigquery_cache", { key: text("key").primaryKey(), sql: text("sql").notNull(), result: text("result").notNull(), bytesProcessed: integer("bytes_processed").notNull().default(0), createdAt: text("created_at").notNull(), expiresAt: text("expires_at").notNull(), }); /** * First-party dashboard panel result cache — see * server/lib/first-party-analytics-cache.ts. */ export const firstPartyAnalyticsCache = table("first_party_analytics_cache", { key: text("key").primaryKey(), sql: text("sql").notNull(), result: text("result").notNull(), createdAt: text("created_at").notNull(), expiresAt: text("expires_at").notNull(), }); /** * Public write keys for the first-party analytics ingestion endpoint. * The key is intentionally public/write-only: it can create events for the * owning user/org but grants no read or admin access. */ export const analyticsPublicKeys = table("analytics_public_keys", { id: text("id").primaryKey(), name: text("name").notNull(), publicKey: text("public_key").notNull(), publicKeyPrefix: text("public_key_prefix").notNull(), replayAllowedOrigins: text("replay_allowed_origins").notNull().default("[]"), replayMaxBytesPerDay: integer("replay_max_bytes_per_day") .notNull() .default(100 * 1024 * 1024), replayMaxRequestsPerMinute: integer("replay_max_requests_per_minute") .notNull() .default(120), createdAt: text("created_at").notNull().default(now()), lastUsedAt: text("last_used_at"), revokedAt: text("revoked_at"), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }); /** * First-party product analytics events recorded via /track. * Common dimensions are mirrored as columns so dashboards can group/filter * without dialect-specific JSON operators. */ export const analyticsEvents = table("analytics_events", { id: text("id").primaryKey(), publicKeyId: text("public_key_id").notNull(), eventName: text("event_name").notNull(), userId: text("user_id"), anonymousId: text("anonymous_id"), userKey: text("user_key"), sessionId: text("session_id"), timestamp: text("timestamp").notNull(), eventDate: text("event_date"), receivedAt: text("received_at").notNull().default(now()), url: text("url"), path: text("path"), hostname: text("hostname"), referrer: text("referrer"), app: text("app"), template: text("template"), signedIn: text("signed_in"), properties: text("properties").notNull().default("{}"), context: text("context").notNull().default("{}"), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }); /** * Generic alert rules over first-party analytics events. Rules are owned by a * user/org but can target any app, template, event name, or event property. */ export const analyticsAlertRules = table("analytics_alert_rules", { id: text("id").primaryKey(), name: text("name").notNull(), description: text("description").notNull().default(""), eventName: text("event_name"), filters: text("filters").notNull().default("[]"), thresholdMode: text("threshold_mode", { enum: ["event_count", "distinct_count"], }) .notNull() .default("event_count"), distinctBy: text("distinct_by"), threshold: integer("threshold").notNull().default(1), windowMinutes: integer("window_minutes").notNull().default(10), cooldownMinutes: integer("cooldown_minutes").notNull().default(30), severity: text("severity", { enum: ["warning", "critical"] }) .notNull() .default("warning"), channels: text("channels").notNull().default('["inbox"]'), emailRecipients: text("email_recipients").notNull().default("[]"), /** Optional per-rule Slack incoming webhook URL (overrides workspace env). */ slackWebhookUrl: text("slack_webhook_url"), /** Optional per-rule generic webhook URL (overrides workspace env). */ webhookUrl: text("webhook_url"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), lastEvaluatedAt: text("last_evaluated_at"), lastTriggeredAt: text("last_triggered_at"), lastStatus: text("last_status", { enum: ["ok", "triggered", "cooldown", "error", "running"], }), lastError: text("last_error"), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }); export const analyticsAlertIncidents = table("analytics_alert_incidents", { id: text("id").primaryKey(), ruleId: text("rule_id").notNull(), triggeredAt: text("triggered_at").notNull(), windowStart: text("window_start").notNull(), windowEnd: text("window_end").notNull(), threshold: integer("threshold").notNull(), observedValue: integer("observed_value").notNull(), eventCount: integer("event_count").notNull(), severity: text("severity", { enum: ["warning", "critical"] }).notNull(), channels: text("channels").notNull().default("[]"), sampleEvents: text("sample_events").notNull().default("[]"), notificationId: text("notification_id"), createdAt: text("created_at").notNull().default(now()), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }); /** * Admin-only registry of external agent-native app databases that Analytics can * inspect. Secret values live in app_secrets; this table stores metadata and * secret keys scoped to the active organization. */ export const analyticsDbAdminConnections = table( "analytics_db_admin_connections", { id: text("id").primaryKey(), name: text("name").notNull(), appId: text("app_id"), appUrl: text("app_url"), databaseUrlSecretKey: text("database_url_secret_key").notNull(), databaseAuthTokenSecretKey: text("database_auth_token_secret_key"), createdBy: text("created_by").notNull(), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), orgId: text("org_id").notNull(), }, (connection) => ({ orgUpdatedIdx: index("analytics_db_admin_connections_org_updated_idx").on( connection.orgId, connection.updatedAt, ), }), ); /** * Session replay summaries recorded through the first-party analytics replay * endpoint. Raw replay chunks live in session_replay_chunks and are only read * through scoped replay helpers, not first-party dashboard SQL. */ export const sessionRecordings = table("session_recordings", { id: text("id").primaryKey(), publicKeyId: text("public_key_id").notNull(), clientRecordingId: text("client_recording_id").notNull(), sessionId: text("session_id").notNull(), userId: text("user_id"), anonymousId: text("anonymous_id"), userKey: text("user_key"), startedAt: text("started_at").notNull(), endedAt: text("ended_at"), durationMs: integer("duration_ms"), chunkCount: integer("chunk_count").notNull().default(0), eventCount: integer("event_count").notNull().default(0), totalBytes: integer("total_bytes").notNull().default(0), pageCount: integer("page_count").notNull().default(0), errorCount: integer("error_count").notNull().default(0), // Additive column: failed network requests (status >= 400 or status 0) // observed in captured replay diagnostics events. networkErrorCount: integer("network_error_count").notNull().default(0), rageClickCount: integer("rage_click_count").notNull().default(0), privacyMode: text("privacy_mode").notNull().default("unknown"), firstUrl: text("first_url"), lastUrl: text("last_url"), path: text("path"), hostname: text("hostname"), referrer: text("referrer"), app: text("app"), template: text("template"), status: text("status", { enum: ["active", "completed"] }) .notNull() .default("active"), metadata: text("metadata").notNull().default("{}"), createdAt: text("created_at").notNull().default(now()), updatedAt: text("updated_at").notNull().default(now()), lastIngestedAt: text("last_ingested_at"), ...ownableColumns(), }); export const sessionRecordingShares = createSharesTable( "session_recording_shares", ); export const sessionReplayChunks = table( "session_replay_chunks", { id: text("id").primaryKey(), recordingId: text("recording_id").notNull(), seq: integer("seq").notNull(), checksum: text("checksum").notNull(), byteLength: integer("byte_length").notNull().default(0), eventCount: integer("event_count").notNull().default(0), startedAt: text("started_at"), endedAt: text("ended_at"), storageKind: text("storage_kind", { enum: ["inline", "blob"] }).notNull(), storageRef: text("storage_ref"), inlineData: text("inline_data"), createdAt: text("created_at").notNull().default(now()), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }, (chunk) => ({ recordingSeqUnique: uniqueIndex( "session_replay_chunks_recording_seq_idx", ).on(chunk.recordingId, chunk.seq), }), ); export const sessionReplayIngests = table( "session_replay_ingests", { id: text("id").primaryKey(), publicKeyId: text("public_key_id").notNull(), recordingId: text("recording_id").notNull(), byteLength: integer("byte_length").notNull().default(0), createdAt: text("created_at").notNull().default(now()), ownerEmail: text("owner_email").notNull().default("local@localhost"), orgId: text("org_id"), }, (ingest) => ({ publicKeyCreatedAtIdx: index( "session_replay_ingests_public_key_created_at_idx", ).on(ingest.publicKeyId, ingest.createdAt), recordingIdx: index("session_replay_ingests_recording_idx").on( ingest.recordingId, ), }), );