diff --git a/drizzle/0040_plain_saracen.sql b/drizzle/0040_plain_saracen.sql new file mode 100644 index 0000000000..6fcdd08d9c --- /dev/null +++ b/drizzle/0040_plain_saracen.sql @@ -0,0 +1 @@ +ALTER TABLE `audits` ADD `domain_expiration_json` text; \ No newline at end of file diff --git a/drizzle/meta/0040_snapshot.json b/drizzle/meta/0040_snapshot.json new file mode 100644 index 0000000000..a7c59930c4 --- /dev/null +++ b/drizzle/meta/0040_snapshot.json @@ -0,0 +1,3787 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0aebd757-371f-4819-a5f2-25924c146b83", + "prevId": "4e0d624b-1867-4def-aef9-c42f2e21561f", + "tables": { + "analysis_runs": { + "name": "analysis_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "params_json": { + "name": "params_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cache_key": { + "name": "cache_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ran_by": { + "name": "ran_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "analysis_runs_project_feature_idx": { + "name": "analysis_runs_project_feature_idx", + "columns": [ + "project_id", + "feature", + "last_ran_at" + ], + "isUnique": false + }, + "analysis_runs_unique_inputs_idx": { + "name": "analysis_runs_unique_inputs_idx", + "columns": [ + "project_id", + "feature", + "cache_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "analysis_runs_project_id_projects_id_fk": { + "name": "analysis_runs_project_id_projects_id_fk", + "tableFrom": "analysis_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_lighthouse_results": { + "name": "audit_lighthouse_results", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "performance_score": { + "name": "performance_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accessibility_score": { + "name": "accessibility_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "best_practices_score": { + "name": "best_practices_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seo_score": { + "name": "seo_score", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lcp_ms": { + "name": "lcp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cls": { + "name": "cls", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "inp_ms": { + "name": "inp_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ttfb_ms": { + "name": "ttfb_ms", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_size_bytes": { + "name": "payload_size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_lighthouse_results_audit_id_idx": { + "name": "audit_lighthouse_results_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_lighthouse_results_audit_id_audits_id_fk": { + "name": "audit_lighthouse_results_audit_id_audits_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_lighthouse_results_page_id_audit_pages_id_fk": { + "name": "audit_lighthouse_results_page_id_audit_pages_id_fk", + "tableFrom": "audit_lighthouse_results", + "tableTo": "audit_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_pages": { + "name": "audit_pages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_description": { + "name": "meta_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "canonical_url": { + "name": "canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "robots_meta": { + "name": "robots_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_title": { + "name": "og_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_description": { + "name": "og_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "og_image": { + "name": "og_image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "h1_count": { + "name": "h1_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h2_count": { + "name": "h2_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h3_count": { + "name": "h3_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h4_count": { + "name": "h4_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h5_count": { + "name": "h5_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "h6_count": { + "name": "h6_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "heading_order_json": { + "name": "heading_order_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "word_count": { + "name": "word_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_total": { + "name": "images_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_missing_alt": { + "name": "images_missing_alt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "images_json": { + "name": "images_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_link_count": { + "name": "internal_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "external_link_count": { + "name": "external_link_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "has_structured_data": { + "name": "has_structured_data", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hreflang_tags_json": { + "name": "hreflang_tags_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_pages_audit_id_idx": { + "name": "audit_pages_audit_id_idx", + "columns": [ + "audit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_pages_audit_id_audits_id_fk": { + "name": "audit_pages_audit_id_audits_id_fk", + "tableFrom": "audit_pages", + "tableTo": "audits", + "columnsFrom": [ + "audit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audits": { + "name": "audits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_by_user_id": { + "name": "started_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_url": { + "name": "start_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "pages_crawled": { + "name": "pages_crawled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "pages_total": { + "name": "pages_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_total": { + "name": "lighthouse_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_completed": { + "name": "lighthouse_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lighthouse_failed": { + "name": "lighthouse_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_phase": { + "name": "current_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'discovery'" + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_expiration_json": { + "name": "domain_expiration_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audits_project_id_idx": { + "name": "audits_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "audits_started_by_user_id_idx": { + "name": "audits_started_by_user_id_idx", + "columns": [ + "started_by_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audits_project_id_projects_id_fk": { + "name": "audits_project_id_projects_id_fk", + "tableFrom": "audits", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "brand_visibility_snapshots": { + "name": "brand_visibility_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "captured_on": { + "name": "captured_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_mentions": { + "name": "total_mentions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chatgpt_mentions": { + "name": "chatgpt_mentions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "google_mentions": { + "name": "google_mentions", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_share_pct": { + "name": "target_share_pct", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "brand_visibility_snapshots_project_id_idx": { + "name": "brand_visibility_snapshots_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "brand_visibility_snapshots_unique_day_idx": { + "name": "brand_visibility_snapshots_unique_day_idx", + "columns": [ + "project_id", + "target", + "captured_on" + ], + "isUnique": true + } + }, + "foreignKeys": { + "brand_visibility_snapshots_project_id_projects_id_fk": { + "name": "brand_visibility_snapshots_project_id_projects_id_fk", + "tableFrom": "brand_visibility_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gbp_connections": { + "name": "gbp_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_name": { + "name": "location_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "gbp_connections_project_idx": { + "name": "gbp_connections_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + }, + "gbp_connections_organization_idx": { + "name": "gbp_connections_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "gbp_connections_project_id_projects_id_fk": { + "name": "gbp_connections_project_id_projects_id_fk", + "tableFrom": "gbp_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gbp_connections_organization_id_organization_id_fk": { + "name": "gbp_connections_organization_id_organization_id_fk", + "tableFrom": "gbp_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gbp_scheduled_posts": { + "name": "gbp_scheduled_posts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "media_url": { + "name": "media_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_to_action_type": { + "name": "call_to_action_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "call_to_action_url": { + "name": "call_to_action_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "published_post_id": { + "name": "published_post_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "gbp_scheduled_posts_project_idx": { + "name": "gbp_scheduled_posts_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "gbp_scheduled_posts_status_scheduled_idx": { + "name": "gbp_scheduled_posts_status_scheduled_idx", + "columns": [ + "status", + "scheduled_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "gbp_scheduled_posts_project_id_projects_id_fk": { + "name": "gbp_scheduled_posts_project_id_projects_id_fk", + "tableFrom": "gbp_scheduled_posts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "geo_locations": { + "name": "geo_locations", + "columns": { + "code": { + "name": "code", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state_code": { + "name": "state_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_metro_code": { + "name": "parent_metro_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "country_code": { + "name": "country_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "population": { + "name": "population", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "geo_locations_country_name_idx": { + "name": "geo_locations_country_name_idx", + "columns": [ + "country_code", + "name" + ], + "isUnique": false + }, + "geo_locations_type_idx": { + "name": "geo_locations_type_idx", + "columns": [ + "type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyword_fit_verdicts": { + "name": "keyword_fit_verdicts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "verdict": { + "name": "verdict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "keyword_fit_verdicts_project_keyword_idx": { + "name": "keyword_fit_verdicts_project_keyword_idx", + "columns": [ + "project_id", + "keyword" + ], + "isUnique": true + } + }, + "foreignKeys": { + "keyword_fit_verdicts_project_id_projects_id_fk": { + "name": "keyword_fit_verdicts_project_id_projects_id_fk", + "tableFrom": "keyword_fit_verdicts", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyword_metrics": { + "name": "keyword_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "competition": { + "name": "competition", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_searches": { + "name": "monthly_searches", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "keyword_metrics_unique_project_keyword_location_language": { + "name": "keyword_metrics_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "keyword_metrics_lookup_idx": { + "name": "keyword_metrics_lookup_idx", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code", + "fetched_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "keyword_metrics_project_id_projects_id_fk": { + "name": "keyword_metrics_project_id_projects_id_fk", + "tableFrom": "keyword_metrics", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "page_optimizations": { + "name": "page_optimizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "element": { + "name": "element", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "current_value": { + "name": "current_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "suggested_value": { + "name": "suggested_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'rules'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "page_optimizations_project_id_idx": { + "name": "page_optimizations_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "page_optimizations_unique_element_idx": { + "name": "page_optimizations_unique_element_idx", + "columns": [ + "project_id", + "url", + "element", + "target" + ], + "isUnique": true + } + }, + "foreignKeys": { + "page_optimizations_project_id_projects_id_fk": { + "name": "page_optimizations_project_id_projects_id_fk", + "tableFrom": "page_optimizations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_competitors": { + "name": "project_competitors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "project_competitors_project_domain_idx": { + "name": "project_competitors_project_domain_idx", + "columns": [ + "project_id", + "domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_competitors_project_id_projects_id_fk": { + "name": "project_competitors_project_id_projects_id_fk", + "tableFrom": "project_competitors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_profiles": { + "name": "project_profiles", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "offer": { + "name": "offer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "customer": { + "name": "customer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "brand_terms": { + "name": "brand_terms", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "service_area_kind": { + "name": "service_area_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'national'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "drafted_at": { + "name": "drafted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "project_profiles_project_idx": { + "name": "project_profiles_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_profiles_project_id_projects_id_fk": { + "name": "project_profiles_project_id_projects_id_fk", + "tableFrom": "project_profiles", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_target_areas": { + "name": "project_target_areas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_country_code": { + "name": "parent_country_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "project_target_areas_project_idx": { + "name": "project_target_areas_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_target_areas_one_primary_per_project_idx": { + "name": "project_target_areas_one_primary_per_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true, + "where": "\"project_target_areas\".\"is_primary\" = 1" + } + }, + "foreignKeys": { + "project_target_areas_project_id_projects_id_fk": { + "name": "project_target_areas_project_id_projects_id_fk", + "tableFrom": "project_target_areas", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_one_default_per_organization_idx": { + "name": "projects_one_default_per_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": true, + "where": "\"projects\".\"name\" = 'Default' AND \"projects\".\"domain\" IS NULL AND \"projects\".\"archived_at\" IS NULL" + }, + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "projects_organization_id_organization_id_fk": { + "name": "projects_organization_id_organization_id_fk", + "tableFrom": "projects", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_check_runs": { + "name": "rank_check_runs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "keywords_total": { + "name": "keywords_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "keywords_checked": { + "name": "keywords_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_subset_run": { + "name": "is_subset_run", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "rank_check_runs_config_idx": { + "name": "rank_check_runs_config_idx", + "columns": [ + "config_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_project_idx": { + "name": "rank_check_runs_project_idx", + "columns": [ + "project_id", + "started_at" + ], + "isUnique": false + }, + "rank_check_runs_one_active_per_config_idx": { + "name": "rank_check_runs_one_active_per_config_idx", + "columns": [ + "config_id" + ], + "isUnique": true, + "where": "\"rank_check_runs\".\"status\" IN ('pending', 'running')" + } + }, + "foreignKeys": { + "rank_check_runs_config_id_rank_tracking_configs_id_fk": { + "name": "rank_check_runs_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rank_check_runs_project_id_projects_id_fk": { + "name": "rank_check_runs_project_id_projects_id_fk", + "tableFrom": "rank_check_runs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_snapshots": { + "name": "rank_snapshots", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tracking_keyword_id": { + "name": "tracking_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device": { + "name": "device", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serp_features": { + "name": "serp_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_snapshots_keyword_device_idx": { + "name": "rank_snapshots_keyword_device_idx", + "columns": [ + "tracking_keyword_id", + "device", + "checked_at" + ], + "isUnique": false + }, + "rank_snapshots_run_keyword_device_idx": { + "name": "rank_snapshots_run_keyword_device_idx", + "columns": [ + "run_id", + "tracking_keyword_id", + "device" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_snapshots_run_id_rank_check_runs_id_fk": { + "name": "rank_snapshots_run_id_rank_check_runs_id_fk", + "tableFrom": "rank_snapshots", + "tableTo": "rank_check_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_configs": { + "name": "rank_tracking_configs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "devices": { + "name": "devices", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'both'" + }, + "serp_depth": { + "name": "serp_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_interval": { + "name": "schedule_interval", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'weekly'" + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_skip_reason": { + "name": "last_skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_configs_project_domain_location_idx": { + "name": "rank_tracking_configs_project_domain_location_idx", + "columns": [ + "project_id", + "domain", + "location_code" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_configs_project_id_projects_id_fk": { + "name": "rank_tracking_configs_project_id_projects_id_fk", + "tableFrom": "rank_tracking_configs", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rank_tracking_keywords": { + "name": "rank_tracking_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "search_volume": { + "name": "search_volume", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_difficulty": { + "name": "keyword_difficulty", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpc": { + "name": "cpc", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metrics_fetched_at": { + "name": "metrics_fetched_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "rank_tracking_keywords_config_keyword_idx": { + "name": "rank_tracking_keywords_config_keyword_idx", + "columns": [ + "config_id", + "keyword" + ], + "isUnique": true + } + }, + "foreignKeys": { + "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk": { + "name": "rank_tracking_keywords_config_id_rank_tracking_configs_id_fk", + "tableFrom": "rank_tracking_keywords", + "tableTo": "rank_tracking_configs", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tag_assignments": { + "name": "saved_keyword_tag_assignments", + "columns": { + "saved_keyword_id": { + "name": "saved_keyword_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tag_assignments_unique_idx": { + "name": "saved_keyword_tag_assignments_unique_idx", + "columns": [ + "saved_keyword_id", + "tag_id" + ], + "isUnique": true + }, + "saved_keyword_tag_assignments_tag_idx": { + "name": "saved_keyword_tag_assignments_tag_idx", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk": { + "name": "saved_keyword_tag_assignments_saved_keyword_id_saved_keywords_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keywords", + "columnsFrom": [ + "saved_keyword_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk": { + "name": "saved_keyword_tag_assignments_tag_id_saved_keyword_tags_id_fk", + "tableFrom": "saved_keyword_tag_assignments", + "tableTo": "saved_keyword_tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keyword_tags": { + "name": "saved_keyword_tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keyword_tags_project_normalized_name_idx": { + "name": "saved_keyword_tags_project_normalized_name_idx", + "columns": [ + "project_id", + "normalized_name" + ], + "isUnique": true + }, + "saved_keyword_tags_project_name_idx": { + "name": "saved_keyword_tags_project_name_idx", + "columns": [ + "project_id", + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keyword_tags_project_id_projects_id_fk": { + "name": "saved_keyword_tags_project_id_projects_id_fk", + "tableFrom": "saved_keyword_tags", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "saved_keywords": { + "name": "saved_keywords", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "location_code": { + "name": "location_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2840 + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "saved_keywords_unique_project_keyword_location_language": { + "name": "saved_keywords_unique_project_keyword_location_language", + "columns": [ + "project_id", + "keyword", + "location_code", + "language_code" + ], + "isUnique": true + }, + "saved_keywords_project_created_idx": { + "name": "saved_keywords_project_created_idx", + "columns": [ + "project_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "saved_keywords_project_id_projects_id_fk": { + "name": "saved_keywords_project_id_projects_id_fk", + "tableFrom": "saved_keywords", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_onboarding_answers": { + "name": "user_onboarding_answers", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interested_features": { + "name": "interested_features", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "work_for": { + "name": "work_for", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_website_count": { + "name": "client_website_count", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "found_via": { + "name": "found_via", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mcp_setup_intent": { + "name": "mcp_setup_intent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gsc_nudge_dismissed_at": { + "name": "gsc_nudge_dismissed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "user_onboarding_answers_organization_idx": { + "name": "user_onboarding_answers_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_onboarding_answers_user_id_user_id_fk": { + "name": "user_onboarding_answers_user_id_user_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_onboarding_answers_organization_id_organization_id_fk": { + "name": "user_onboarding_answers_organization_id_organization_id_fk", + "tableFrom": "user_onboarding_answers", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sam_project_memory": { + "name": "sam_project_memory", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "sam_project_memory_project_id_projects_id_fk": { + "name": "sam_project_memory_project_id_projects_id_fk", + "tableFrom": "sam_project_memory", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sam_project_memory_project_id_label_pk": { + "columns": [ + "project_id", + "label" + ], + "name": "sam_project_memory_project_id_label_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sam_sessions": { + "name": "sam_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'New chat'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "archived_at": { + "name": "archived_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sam_sessions_project_updated_idx": { + "name": "sam_sessions_project_updated_idx", + "columns": [ + "project_id", + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sam_sessions_project_id_projects_id_fk": { + "name": "sam_sessions_project_id_projects_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sam_sessions_user_id_user_id_fk": { + "name": "sam_sessions_user_id_user_id_fk", + "tableFrom": "sam_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "account_accountId_providerId_idx": { + "name": "account_accountId_providerId_idx", + "columns": [ + "account_id", + "provider_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "analytics_opted_out": { + "name": "analytics_opted_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + }, + "verification_expiresAt_idx": { + "name": "verification_expiresAt_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "billing_customer_status": { + "name": "billing_customer_status", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "is_paying": { + "name": "is_paying", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "paid_plan_id": { + "name": "paid_plan_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_plan_status": { + "name": "paid_plan_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_json": { + "name": "customer_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synced_at": { + "name": "synced_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_customer_status_organization_id_organization_id_fk": { + "name": "billing_customer_status_organization_id_organization_id_fk", + "tableFrom": "billing_customer_status", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gsc_connections": { + "name": "gsc_connections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "site_url": { + "name": "site_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connected_account_email": { + "name": "connected_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": { + "gsc_connections_project_idx": { + "name": "gsc_connections_project_idx", + "columns": [ + "project_id" + ], + "isUnique": true + }, + "gsc_connections_organization_idx": { + "name": "gsc_connections_organization_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "gsc_connections_project_id_projects_id_fk": { + "name": "gsc_connections_project_id_projects_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "gsc_connections_organization_id_organization_id_fk": { + "name": "gsc_connections_organization_id_organization_id_fk", + "tableFrom": "gsc_connections", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "gsc_oauth_config": { + "name": "gsc_oauth_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_encrypted": { + "name": "client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(current_timestamp)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e7c40ba11e..103ab9e7d6 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -281,6 +281,13 @@ "when": 1786382227974, "tag": "0039_ordinary_blazing_skull", "breakpoints": true + }, + { + "idx": 40, + "version": "6", + "when": 1787235975236, + "tag": "0040_plain_saracen", + "breakpoints": true } ] } \ No newline at end of file diff --git a/e2e/audit-domain-expiry.spec.ts b/e2e/audit-domain-expiry.spec.ts new file mode 100644 index 0000000000..f4a1bb7240 --- /dev/null +++ b/e2e/audit-domain-expiry.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from "@playwright/test"; + +/** + * The audit results expiry card, rendered from a run whose registration was + * already looked up. + * + * Requires a completed audit carrying `domain_expiration_json`. Seed one into + * local D1 (free — no crawl, no credits): + * + * npx wrangler d1 execute open-seo --local --command "INSERT INTO audits + * (id, project_id, started_by_user_id, start_url, status, pages_crawled, + * pages_total, completed_at, domain_expiration_json) + * VALUES ('seed-audit-expiry', '', '', + * 'https://deliotx.com', 'completed', 10, 10, '2026-08-20T00:00:00Z', + * '{\"domain\":\"deliotx.com\",\"expirationDate\":\"2028-02-22T19:23:16Z\", + * \"createdDate\":\"2024-02-22T19:23:16Z\",\"lastUpdatedDate\":null}');" + * + * The point of the test is that a stored run renders with ZERO requests: the + * day counts come from the stored ABSOLUTE dates against the current clock, so + * re-opening a months-old audit costs nothing and still states today's truth. + */ +const SEEDED_AUDIT_ID = "seed-audit-expiry"; +const SEEDED_PROJECT_ID = "464954b0-255e-441a-91ce-fbe2b5baebf3"; + +function serverFnTarget(url: string): string { + const segment = url.split("/_serverFn/")[1]; + if (!segment) return ""; + try { + return atob(segment.split("?")[0] ?? ""); + } catch { + return ""; + } +} + +test.describe("Audit domain-expiry card", () => { + // Depends on the seeded row above, which CI does not have. Opt in locally + // with AUDIT_EXPIRY_SEEDED=1 after running the insert. + test.skip( + process.env.AUDIT_EXPIRY_SEEDED !== "1", + "Seed the audit row (see header) and set AUDIT_EXPIRY_SEEDED=1.", + ); + + test("renders stored registration without any metered request", async ({ + page, + }) => { + const expiryCalls: string[] = []; + page.on("request", (request) => { + const target = serverFnTarget(request.url()); + if (target.includes("auditDomainExpiry")) expiryCalls.push(request.url()); + }); + + await page.goto("/"); + await page.waitForURL(/\/p\/([^/]+)\/keywords(?:\?.*)?$/, { + timeout: 30_000, + }); + const match = page.url().match(/\/p\/([^/]+)\/keywords/); + if (!match) throw new Error(`Could not read project id from ${page.url()}`); + + // Pin the project explicitly: the seeded audit belongs to one project, and + // the landing redirect follows whichever project was last selected. + await page.goto(`/p/${SEEDED_PROJECT_ID}/audit?auditId=${SEEDED_AUDIT_ID}`); + + const card = page.getByTestId("audit-domain-expiry"); + await expect(card).toBeVisible({ timeout: 30_000 }); + + // Derived from the stored absolute dates, not stored themselves. + await expect(card).toContainText("Domain registration"); + await expect(card).toContainText("Expires"); + await expect(card).toContainText("Days left"); + await expect(card).toContainText("Healthy"); + // Registered 2024-02-22, so it is comfortably over a year old. + await expect(card).toContainText(/\d+(\.\d+)? yrs/); + + expect( + expiryCalls, + "a stored registration must render without paying again", + ).toHaveLength(0); + }); +}); diff --git a/e2e/backlinks-expiry-column.spec.ts b/e2e/backlinks-expiry-column.spec.ts new file mode 100644 index 0000000000..5e81947290 --- /dev/null +++ b/e2e/backlinks-expiry-column.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from "@playwright/test"; + +/** + * The backlinks expiry column, verified WITHOUT spending anything. + * + * Runs against the offline seed (`pnpm seed:backlinks-run`), whose referring + * domains are synthetic (`source-01.com` …). Looking those up would burn ~145 + * APIVerve credits to learn nothing, so this deliberately stops short of + * clicking: what it proves is the wiring, the billable-count arithmetic, and + * that the cost is quoted before any click. The fetch itself is covered by the + * live expiry spec and by unit tests. + */ +function serverFnTarget(url: string): string { + const segment = url.split("/_serverFn/")[1]; + if (!segment) return ""; + try { + return atob(segment.split("?")[0] ?? ""); + } catch { + return ""; + } +} + +test.describe("Backlinks domain-expiry column", () => { + test("offers a per-page action that quotes its cost and spends nothing", async ({ + page, + }) => { + const expiryCalls: string[] = []; + page.on("request", (request) => { + const target = serverFnTarget(request.url()); + if (target.includes("domainExpiration")) expiryCalls.push(request.url()); + }); + + await page.goto("/"); + await page.waitForURL(/\/p\/([^/]+)\/keywords(?:\?.*)?$/, { + timeout: 30_000, + }); + const match = page.url().match(/\/p\/([^/]+)\/keywords/); + if (!match) throw new Error(`Could not read project id from ${page.url()}`); + + // No target in the URL: the tab restores the seeded run, which is free. + await page.goto(`/p/${match[1]}/backlinks`); + await expect( + page.getByRole("heading", { name: /Backlinks/i }).first(), + ).toBeVisible({ timeout: 30_000 }); + + const dismiss = page.getByRole("button", { name: "Dismiss" }); + if (await dismiss.isVisible()) await dismiss.click(); + + // A restored run shows the overview but not the individual links; the seed + // wrote the links slice to the cache, so loading them is a cache hit. + const loadLinks = page.getByRole("button", { + name: /Refresh & load links/i, + }); + if (await loadLinks.isVisible().catch(() => false)) { + await loadLinks.click(); + } + + await page + .getByRole("button", { name: "Backlinks table actions" }) + .click({ timeout: 45_000 }); + + // The count is what THIS click would cost, and it must be stated up front. + const expiryItem = page.getByText(/Domain expiry \(\d+ × 5 credits\)/); + await expect(expiryItem).toBeVisible({ timeout: 15_000 }); + + const label = (await expiryItem.textContent()) ?? ""; + const quoted = Number(/\((\d+) ×/.exec(label)?.[1] ?? "0"); + expect(quoted).toBeGreaterThan(0); + + // Opening the menu must not have billed anything. + expect( + expiryCalls, + "the expiry action must not fire before it is clicked", + ).toHaveLength(0); + }); +}); diff --git a/src/client/features/audit/results/AuditDomainExpiryCard.tsx b/src/client/features/audit/results/AuditDomainExpiryCard.tsx new file mode 100644 index 0000000000..fa9a285285 --- /dev/null +++ b/src/client/features/audit/results/AuditDomainExpiryCard.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +import { z } from "zod"; +import { saveAuditDomainExpiration } from "@/serverFunctions/auditDomainExpiry"; +import { + createMeteredRunKey, + useAuthorizedRun, + useMeteredQuery, +} from "@/client/lib/useMeteredQuery"; +import { InlineQueryError } from "@/client/components/InlineQueryError"; +import { getStandardErrorMessage } from "@/client/lib/error-messages"; +import { + deriveDomainExpiration, + type DomainExpirationFacts, + type DomainExpirationStatus, +} from "@/shared/domainExpiration"; +import { Button } from "@cloudflare/kumo/components/button"; +import { Loader } from "@cloudflare/kumo/components/loader"; + +const STATUS_LABELS: Record = { + expired: "Expired", + critical: "Expires soon", + warning: "Renew this quarter", + healthy: "Healthy", +}; + +const STATUS_CLASSES: Record = { + expired: "text-error", + critical: "text-error", + warning: "text-warning", + healthy: "text-success", +}; + +const storedFactsSchema = z.object({ + domain: z.string(), + expirationDate: z.string().nullable().catch(null), + createdDate: z.string().nullable().catch(null), + lastUpdatedDate: z.string().nullable().catch(null), +}); + +/** + * Parses the facts pinned to the audit row. + * + * Returns null on anything unexpected rather than throwing: a row written by an + * older build must degrade to "not looked up yet", never break the results page. + */ +function parseStoredFacts(raw: string | null): DomainExpirationFacts | null { + if (!raw) return null; + try { + const parsed = storedFactsSchema.safeParse(JSON.parse(raw)); + return parsed.success ? parsed.data : null; + } catch { + return null; + } +} + +function formatDate(value: string | null): string { + if (!value) return "—"; + const ms = Date.parse(value); + if (!Number.isFinite(ms)) return "—"; + return new Date(ms).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +/** + * Domain registration for the audited site, pinned to the audit so it reaches + * the client report. + * + * Costs 5 APIVerve credits and is therefore an explicit click, never part of + * the crawl -- the audit's consent covers crawling, not a third-party lookup. + * + * A stored result renders immediately with NO request: the day counts are + * recomputed from the stored absolute dates against the current clock, so + * re-opening a months-old audit shows what is true today without paying again. + */ +export function AuditDomainExpiryCard({ + projectId, + auditId, + storedFactsJson, +}: { + projectId: string; + auditId: string; + storedFactsJson: string | null; +}) { + const [storedFacts] = useState(() => parseStoredFacts(storedFactsJson)); + const run = useAuthorizedRun(createMeteredRunKey(projectId, auditId, 1)); + const expiryQuery = useMeteredQuery({ + authorized: run.authorized, + runNonce: run.runNonce, + queryKey: ["audit-domain-expiry", projectId, auditId], + queryFn: () => saveAuditDomainExpiration({ data: { projectId, auditId } }), + }); + + const fresh = expiryQuery.data ?? null; + const derived = + fresh ?? + (storedFacts ? deriveDomainExpiration(storedFacts, Date.now()) : null); + const status = derived?.status ?? null; + + return ( +
+
+
+

+ Domain registration +

+ {status ? ( + + {STATUS_LABELS[status]} + + ) : null} +
+ + {derived ? ( +
+
+
Expires
+
+ {formatDate(derived.expirationDate)} +
+
+
+
Days left
+
+ {derived.daysToExpiration == null + ? "—" + : derived.daysToExpiration.toLocaleString()} +
+
+
+
Age
+
+ {derived.domainAgeYears == null + ? "—" + : `${derived.domainAgeYears.toLocaleString()} yrs`} +
+
+
+ ) : run.authorized && expiryQuery.isLoading ? ( +
+ +
+ ) : run.authorized && expiryQuery.isError ? ( + void expiryQuery.refetch()} + /> + ) : ( +
+

+ Adds the domain's expiry and age to this audit and to the + client report. Costs 5 APIVerve credits, then free for 7 days. +

+ +
+ )} +
+
+ ); +} diff --git a/src/client/features/audit/results/ResultsView.tsx b/src/client/features/audit/results/ResultsView.tsx index 62f2790324..1d9b2c9a99 100644 --- a/src/client/features/audit/results/ResultsView.tsx +++ b/src/client/features/audit/results/ResultsView.tsx @@ -4,6 +4,7 @@ import { Link } from "@tanstack/react-router"; import { Warning } from "@phosphor-icons/react"; import { InsightIcon } from "@/client/components/InsightTile"; import { extractPathname, StatCard } from "@/client/features/audit/shared"; +import { AuditDomainExpiryCard } from "@/client/features/audit/results/AuditDomainExpiryCard"; import { exportPages, exportPerformance, @@ -148,6 +149,12 @@ export function ResultsView({ }} /> + + {activeTab === "pages" && } {activeTab === "performance" && lighthouse.length > 0 && ( { + it("says nothing when the lookup was never run", () => { + expect(buildDomainRegistrationLine(null, NOW)).toBeNull(); + }); + + it("says nothing rather than guessing when the stored value is unusable", () => { + expect(buildDomainRegistrationLine("{ not json", NOW)).toBeNull(); + expect(buildDomainRegistrationLine("{}", NOW)).toBeNull(); + }); + + it("states age and remaining time for a healthy domain", () => { + const line = buildDomainRegistrationLine(factsJson(551), NOW); + expect(line).toContain("10 years old"); + expect(line).toContain("551 days"); + }); + + // A report is a client deliverable: an imminent lapse is the one case that + // has to read as urgent rather than as another neutral statistic. + it("flags an imminent lapse", () => { + const line = buildDomainRegistrationLine(factsJson(12), NOW); + expect(line).toMatch(/renew/i); + }); + + it("flags an already-lapsed domain", () => { + const line = buildDomainRegistrationLine(factsJson(-3), NOW); + expect(line).toMatch(/expired/i); + }); + + // The stored dates are absolute, so the same row must read differently as + // time passes -- that is the whole reason day counts are not stored. + it("recomputes against the reader's clock, not the audit date", () => { + const facts = factsJson(40); + expect(buildDomainRegistrationLine(facts, NOW)).toContain("40 days"); + expect(buildDomainRegistrationLine(facts, NOW + 30 * DAY_MS)).toContain( + "10 days", + ); + }); +}); diff --git a/src/client/features/report/domainRegistrationLine.ts b/src/client/features/report/domainRegistrationLine.ts new file mode 100644 index 0000000000..5830e14a3c --- /dev/null +++ b/src/client/features/report/domainRegistrationLine.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import { deriveDomainExpiration } from "@/shared/domainExpiration"; + +/** + * The client report's one sentence about domain registration. + * + * Pure and clock-injected so it can be tested: the whole point of storing only + * ABSOLUTE dates on the audit row is that this sentence changes as time passes, + * and a report opened three months after the audit must say what is true then. + * + * Returns `null` — meaning "print nothing" — when the lookup was never run or + * the stored value cannot be trusted. A client deliverable should stay silent + * rather than guess about something the reader may act on. + */ +const storedFactsSchema = z.object({ + domain: z.string(), + expirationDate: z.string().nullable().catch(null), + createdDate: z.string().nullable().catch(null), + lastUpdatedDate: z.string().nullable().catch(null), +}); + +export function buildDomainRegistrationLine( + storedFactsJson: string | null, + nowMs: number, +): string | null { + if (!storedFactsJson) return null; + + let facts; + try { + const parsed = storedFactsSchema.safeParse(JSON.parse(storedFactsJson)); + if (!parsed.success) return null; + facts = parsed.data; + } catch { + return null; + } + + const expiry = deriveDomainExpiration(facts, nowMs); + const days = expiry.daysToExpiration; + const years = expiry.domainAgeYears; + if (days == null && years == null) return null; + + const age = + years == null + ? null + : `${years % 1 === 0 ? years.toFixed(0) : years.toFixed(1)} years old`; + + if (days == null) { + return age ? `The domain is ${age}.` : null; + } + + // Three registers, because a report is read by a client: expired and + // near-expiry need to prompt an action, everything else is context. + if (expiry.status === "expired") { + return age + ? `The domain registration has EXPIRED. It is ${age} and needs renewing immediately to avoid losing it.` + : `The domain registration has EXPIRED and needs renewing immediately to avoid losing it.`; + } + + if (expiry.status === "critical") { + return age + ? `The domain is ${age} and expires in ${days.toLocaleString()} days — renew it now.` + : `The domain expires in ${days.toLocaleString()} days — renew it now.`; + } + + return age + ? `The domain is ${age}, with ${days.toLocaleString()} days left on its registration.` + : `The domain has ${days.toLocaleString()} days left on its registration.`; +} diff --git a/src/client/features/report/reportChapters.test.ts b/src/client/features/report/reportChapters.test.ts index ec8f4ddd1b..d4b2787196 100644 --- a/src/client/features/report/reportChapters.test.ts +++ b/src/client/features/report/reportChapters.test.ts @@ -45,6 +45,7 @@ function data(overrides: Partial = {}): Data { refreshKeywordDetails: () => {}, refreshBacklinkDetails: () => {}, latestAudit: null, + auditDomainExpirationJson: null, auditPages: [], approvedFixes: [], brandVisibility: null, diff --git a/src/client/features/report/reportChaptersSite.tsx b/src/client/features/report/reportChaptersSite.tsx index 2d0727c228..47710331e2 100644 --- a/src/client/features/report/reportChaptersSite.tsx +++ b/src/client/features/report/reportChaptersSite.tsx @@ -1,4 +1,5 @@ import { buildBacklinkNarrative } from "@/client/features/report/reportNarrative"; +import { buildDomainRegistrationLine } from "@/client/features/report/domainRegistrationLine"; import { ReportBreakdownCard, ReportCallout, @@ -74,8 +75,20 @@ function buildImprovementChapters( { data, sections, technicalIssues }: ChapterInput, out: ChapterCollector, ): void { - const { latestAudit, auditPages, approvedFixes, readFailures } = data; + const { + latestAudit, + auditPages, + approvedFixes, + readFailures, + auditDomainExpirationJson, + } = data; const pagesCrawled = latestAudit?.pagesCrawled ?? null; + // Null unless someone explicitly ran the lookup on the audit -- the report + // never spends anything itself, so it simply omits the sentence. + const domainRegistrationLine = buildDomainRegistrationLine( + auditDomainExpirationJson, + Date.now(), + ); // `latestAudit` is a `.find()` over `auditsQuery.data ?? []`, so a history // read that threw is byte-for-byte "no audit has ever completed". This is // the only place that difference still exists, and both chapters below @@ -98,6 +111,9 @@ function buildImprovementChapters( : "On-page optimizations", body: ( <> + {domainRegistrationLine ? ( + {domainRegistrationLine} + ) : null} {approvedFixes.length > 0 ? ( <> diff --git a/src/client/features/report/useClientReportData.ts b/src/client/features/report/useClientReportData.ts index ca7fddd006..aa043cf81f 100644 --- a/src/client/features/report/useClientReportData.ts +++ b/src/client/features/report/useClientReportData.ts @@ -198,6 +198,12 @@ export function useClientReportData(projectId: string) { // chapter printing the second sentence for the first cause. latestAudit: latestAudit ?? null, auditPages: auditResultsQuery.data?.pages ?? [], + // Taken from the audit RESULTS query rather than the history one: results + // already carry the row, and the value is only ever populated by an + // explicit click, so a report can show it without ever having spent + // anything itself. + auditDomainExpirationJson: + auditResultsQuery.data?.audit?.domainExpirationJson ?? null, approvedFixes: (onPageQuery.data?.rows ?? []).filter( (row) => row.status === "approved", ), diff --git a/src/db/app.schema.ts b/src/db/app.schema.ts index 62c278fa9a..2051ca6597 100644 --- a/src/db/app.schema.ts +++ b/src/db/app.schema.ts @@ -380,6 +380,19 @@ export const audits = sqliteTable( .notNull() .default(sql`(current_timestamp)`), completedAt: text("completed_at"), + /** + * The audited domain's registration facts at the time someone asked for + * them, as JSON: `{ domain, expirationDate, createdDate, lastUpdatedDate }`. + * + * ABSOLUTE dates only, never the day counts -- those are recomputed from + * the clock on read, so a report opened months later states the truth today + * rather than the truth on audit day. Same rule as the KV cache. + * + * Nullable and populated on demand, NOT during the crawl: resolving it + * bills APIVerve, and an audit's consent covers the crawl, not a + * third-party lookup the user never asked for. + */ + domainExpirationJson: text("domain_expiration_json"), }, (table) => [ index("audits_project_id_idx").on(table.projectId), diff --git a/src/db/pg/app.schema.ts b/src/db/pg/app.schema.ts index dfc214e1ac..8702fe8311 100644 --- a/src/db/pg/app.schema.ts +++ b/src/db/pg/app.schema.ts @@ -371,6 +371,19 @@ export const audits = pgTable( currentPhase: text("current_phase").default("discovery"), startedAt: timestampColumn("started_at").notNull().default(isoNow), completedAt: timestampColumn("completed_at"), + /** + * The audited domain's registration facts at the time someone asked for + * them, as JSON: `{ domain, expirationDate, createdDate, lastUpdatedDate }`. + * + * ABSOLUTE dates only, never the day counts -- those are recomputed from + * the clock on read, so a report opened months later states the truth today + * rather than the truth on audit day. Same rule as the KV cache. + * + * Nullable and populated on demand, NOT during the crawl: resolving it + * bills APIVerve, and an audit's consent covers the crawl, not a + * third-party lookup the user never asked for. + */ + domainExpirationJson: text("domain_expiration_json"), }, (table) => [ index("audits_project_id_idx").on(table.projectId), diff --git a/src/server/features/audit/repositories/AuditRepository.ts b/src/server/features/audit/repositories/AuditRepository.ts index a01a0751c9..c401cb7cfd 100644 --- a/src/server/features/audit/repositories/AuditRepository.ts +++ b/src/server/features/audit/repositories/AuditRepository.ts @@ -279,6 +279,23 @@ async function deleteAuditForProject(auditId: string, projectId: string) { .where(and(eq(audits.id, auditId), eq(audits.projectId, projectId))); } +/** + * Store the audited domain's registration facts on the audit row. + * + * Scoped by projectId as well as auditId so one project can never write onto + * another's audit, matching every other accessor in this repository. + */ +async function setAuditDomainExpiration( + auditId: string, + projectId: string, + factsJson: string, +): Promise { + await db + .update(audits) + .set({ domainExpirationJson: factsJson }) + .where(and(eq(audits.id, auditId), eq(audits.projectId, projectId))); +} + export const AuditRepository = { createAudit, updateAuditProgress, @@ -287,6 +304,7 @@ export const AuditRepository = { getAuditForWorkflow, batchWriteResults, getAuditForProject, + setAuditDomainExpiration, getAuditsByProject, getAuditUsageForUser, getAuditResultsForProject, diff --git a/src/server/features/audit/services/AuditService.ts b/src/server/features/audit/services/AuditService.ts index fcf04da332..a065617ea1 100644 --- a/src/server/features/audit/services/AuditService.ts +++ b/src/server/features/audit/services/AuditService.ts @@ -139,6 +139,10 @@ async function getResults(auditId: string, projectId: string) { startedAt: audit.startedAt, completedAt: audit.completedAt, config: parsedConfig, + // Raw JSON, deliberately not parsed here: it holds only ABSOLUTE dates, + // and the day counts are derived against the reader's clock so a + // months-old audit reports what is true today, not on audit day. + domainExpirationJson: audit.domainExpirationJson, }, pages, lighthouse, diff --git a/src/serverFunctions/auditDomainExpiry.ts b/src/serverFunctions/auditDomainExpiry.ts new file mode 100644 index 0000000000..0e3232caec --- /dev/null +++ b/src/serverFunctions/auditDomainExpiry.ts @@ -0,0 +1,72 @@ +import { createServerFn } from "@tanstack/react-start"; +import { env } from "cloudflare:workers"; +import { z } from "zod"; +import { AuditRepository } from "@/server/features/audit/repositories/AuditRepository"; +import { + resolveDomainExpiration, + type ExpirationCache, +} from "@/server/lib/apiverve/domainExpiration"; +import { AppError } from "@/server/lib/errors"; +import { requireProjectContext } from "@/serverFunctions/middleware"; + +/** + * Resolve the audited domain's registration and pin it to the audit row, so it + * travels into the client report. + * + * Explicit, not part of the crawl. Resolving bills 5 APIVerve credits, and an + * audit's consent covers the crawl -- running a third-party lookup the user + * never asked for, on every audit, would be exactly the kind of auto-spend this + * codebase keeps getting bitten by. + * + * Only the ABSOLUTE dates are stored. The day counts are recomputed from the + * clock wherever the row is read, so a report opened three months later states + * what is true then rather than what was true on audit day. + */ +const inputSchema = z.object({ + projectId: z.string().min(1), + auditId: z.string().min(1), +}); + +export const saveAuditDomainExpiration = createServerFn({ method: "POST" }) + .middleware(requireProjectContext) + .validator(inputSchema) + .handler(async ({ data, context }) => { + const audit = await AuditRepository.getAuditForProject( + data.auditId, + context.projectId, + ); + if (!audit) { + throw new AppError("NOT_FOUND", "Audit not found for this project"); + } + + const target = audit.startUrl ?? context.project.domain; + if (!target) { + throw new AppError( + "PROJECT_DOMAIN_MISSING", + "This audit has no domain to look up", + ); + } + + const cache: ExpirationCache = { + get: (key) => env.KV.get(key), + put: (key, value, options) => env.KV.put(key, value, options), + }; + + // Normalization to the registrable domain happens inside the resolver -- + // the audit's startUrl is a full URL, and this is the one choke point that + // turns it into the key the cache and every other caller already share. + const expiration = await resolveDomainExpiration(target, cache, Date.now()); + + await AuditRepository.setAuditDomainExpiration( + data.auditId, + context.projectId, + JSON.stringify({ + domain: expiration.domain, + expirationDate: expiration.expirationDate, + createdDate: expiration.createdDate, + lastUpdatedDate: expiration.lastUpdatedDate, + }), + ); + + return expiration; + });