197 lines
6.8 KiB
TypeScript
197 lines
6.8 KiB
TypeScript
import {
|
|
sqliteTable,
|
|
text,
|
|
integer,
|
|
real,
|
|
index,
|
|
} from "drizzle-orm/sqlite-core";
|
|
import { sql } from "drizzle-orm";
|
|
import { projects } from "./app.schema";
|
|
|
|
// ============================================================================
|
|
// Site Audit tables
|
|
// ============================================================================
|
|
|
|
// One row per audit run
|
|
export const audits = sqliteTable(
|
|
"audits",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
projectId: text("project_id")
|
|
.notNull()
|
|
.references(() => projects.id, { onDelete: "cascade" }),
|
|
startedByUserId: text("started_by_user_id").notNull(),
|
|
startUrl: text("start_url").notNull(),
|
|
status: text("status", {
|
|
enum: ["running", "completed", "failed"],
|
|
})
|
|
.notNull()
|
|
.default("running"),
|
|
workflowInstanceId: text("workflow_instance_id"),
|
|
// JSON config: { maxPages, lighthouseStrategy }
|
|
config: text("config").notNull().default("{}"),
|
|
// Progress & summary
|
|
pagesCrawled: integer("pages_crawled").notNull().default(0),
|
|
pagesTotal: integer("pages_total").notNull().default(0),
|
|
lighthouseTotal: integer("lighthouse_total").notNull().default(0),
|
|
lighthouseCompleted: integer("lighthouse_completed").notNull().default(0),
|
|
lighthouseFailed: integer("lighthouse_failed").notNull().default(0),
|
|
currentPhase: text("current_phase").default("discovery"),
|
|
startedAt: text("started_at")
|
|
.notNull()
|
|
.default(sql`(current_timestamp)`),
|
|
completedAt: text("completed_at"),
|
|
},
|
|
(table) => [
|
|
index("audits_project_id_idx").on(table.projectId),
|
|
index("audits_started_by_user_id_idx").on(table.startedByUserId),
|
|
],
|
|
);
|
|
|
|
// One row per crawled page
|
|
export const auditPages = sqliteTable(
|
|
"audit_pages",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
auditId: text("audit_id")
|
|
.notNull()
|
|
.references(() => audits.id, { onDelete: "cascade" }),
|
|
url: text("url").notNull(),
|
|
statusCode: integer("status_code"),
|
|
redirectUrl: text("redirect_url"),
|
|
// Metadata
|
|
title: text("title"),
|
|
metaDescription: text("meta_description"),
|
|
canonicalUrl: text("canonical_url"),
|
|
robotsMeta: text("robots_meta"),
|
|
// Open Graph
|
|
ogTitle: text("og_title"),
|
|
ogDescription: text("og_description"),
|
|
ogImage: text("og_image"),
|
|
// Headings
|
|
h1Count: integer("h1_count").notNull().default(0),
|
|
h2Count: integer("h2_count").notNull().default(0),
|
|
h3Count: integer("h3_count").notNull().default(0),
|
|
h4Count: integer("h4_count").notNull().default(0),
|
|
h5Count: integer("h5_count").notNull().default(0),
|
|
h6Count: integer("h6_count").notNull().default(0),
|
|
headingOrderJson: text("heading_order_json"),
|
|
// Content
|
|
wordCount: integer("word_count").notNull().default(0),
|
|
// Images
|
|
imagesTotal: integer("images_total").notNull().default(0),
|
|
imagesMissingAlt: integer("images_missing_alt").notNull().default(0),
|
|
imagesJson: text("images_json"),
|
|
// Links
|
|
internalLinkCount: integer("internal_link_count").notNull().default(0),
|
|
externalLinkCount: integer("external_link_count").notNull().default(0),
|
|
// Structured data
|
|
hasStructuredData: integer("has_structured_data", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
// Hreflang
|
|
hreflangTagsJson: text("hreflang_tags_json"),
|
|
// Indexability
|
|
isIndexable: integer("is_indexable", { mode: "boolean" })
|
|
.notNull()
|
|
.default(true),
|
|
// Indexability/canonical signals from response headers
|
|
xRobotsTag: text("x_robots_tag"),
|
|
headerCanonicalUrl: text("header_canonical_url"),
|
|
// Crawl metadata
|
|
// null depth = not reached via links (e.g. sitemap-seeded)
|
|
crawlDepth: integer("crawl_depth"),
|
|
inSitemap: integer("in_sitemap", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
// SHA-256 of the visible body text, for duplicate-content grouping
|
|
contentHash: text("content_hash"),
|
|
// How the fetch resolved: ok | blocked (WAF/bot challenge) | error
|
|
fetchClass: text("fetch_class", { enum: ["ok", "blocked", "error"] })
|
|
.notNull()
|
|
.default("ok"),
|
|
// Performance
|
|
responseTimeMs: integer("response_time_ms"),
|
|
},
|
|
(table) => [index("audit_pages_audit_url_idx").on(table.auditId, table.url)],
|
|
);
|
|
|
|
// One row per unique (source page, target URL) link edge. Currently only
|
|
// internal edges are stored (see AuditRepository); isInternal stays so
|
|
// external-link checks can start writing rows without a migration.
|
|
export const auditLinks = sqliteTable(
|
|
"audit_links",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
auditId: text("audit_id")
|
|
.notNull()
|
|
.references(() => audits.id, { onDelete: "cascade" }),
|
|
sourcePageId: text("source_page_id")
|
|
.notNull()
|
|
.references(() => auditPages.id, { onDelete: "cascade" }),
|
|
sourceUrl: text("source_url").notNull(),
|
|
targetUrl: text("target_url").notNull(),
|
|
anchor: text("anchor"),
|
|
isInternal: integer("is_internal", { mode: "boolean" })
|
|
.notNull()
|
|
.default(true),
|
|
isNofollow: integer("is_nofollow", { mode: "boolean" })
|
|
.notNull()
|
|
.default(false),
|
|
},
|
|
(table) => [
|
|
index("audit_links_audit_target_idx").on(table.auditId, table.targetUrl),
|
|
],
|
|
);
|
|
|
|
// One row per (issue type, affected page)
|
|
export const auditIssues = sqliteTable(
|
|
"audit_issues",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
auditId: text("audit_id")
|
|
.notNull()
|
|
.references(() => audits.id, { onDelete: "cascade" }),
|
|
pageId: text("page_id").references(() => auditPages.id, {
|
|
onDelete: "cascade",
|
|
}),
|
|
pageUrl: text("page_url").notNull(),
|
|
issueType: text("issue_type").notNull(),
|
|
severity: text("severity", { enum: ["critical", "warning", "info"] })
|
|
.notNull()
|
|
.default("info"),
|
|
// JSON details specific to the issue type (e.g. broken link target)
|
|
detailsJson: text("details_json"),
|
|
},
|
|
(table) => [
|
|
index("audit_issues_audit_type_idx").on(table.auditId, table.issueType),
|
|
],
|
|
);
|
|
|
|
// One row per Lighthouse test (mobile + desktop per page).
|
|
export const auditLighthouseResults = sqliteTable(
|
|
"audit_lighthouse_results",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
auditId: text("audit_id")
|
|
.notNull()
|
|
.references(() => audits.id, { onDelete: "cascade" }),
|
|
pageId: text("page_id")
|
|
.notNull()
|
|
.references(() => auditPages.id, { onDelete: "cascade" }),
|
|
strategy: text("strategy", { enum: ["mobile", "desktop"] }).notNull(),
|
|
performanceScore: integer("performance_score"),
|
|
accessibilityScore: integer("accessibility_score"),
|
|
bestPracticesScore: integer("best_practices_score"),
|
|
seoScore: integer("seo_score"),
|
|
lcpMs: real("lcp_ms"),
|
|
cls: real("cls"),
|
|
inpMs: real("inp_ms"),
|
|
ttfbMs: real("ttfb_ms"),
|
|
errorMessage: text("error_message"),
|
|
r2Key: text("r2_key"),
|
|
payloadSizeBytes: integer("payload_size_bytes"),
|
|
},
|
|
(table) => [index("audit_lighthouse_results_audit_id_idx").on(table.auditId)],
|
|
);
|