feat: migrate full Prisma schema to Drizzle ORM (29 tables, 28 enums, 25 relations)

- Install drizzle-orm, drizzle-kit, pg, @types/pg in web/
- Create split schema directory with domain files:
  - auth (users, accounts, sessions, deviceTokens)
  - subscription (familyGroups, familyGroupMembers, subscriptions)
  - darkwatch (watchlistItems, exposures)
  - alerts
  - voiceprint (voiceEnrollments, voiceAnalyses, analysisJobs, analysisResults)
  - spamshield (spamFeedback, spamRules)
  - audit (auditLogs, kpiSnapshots)
  - correlation (normalizedAlerts, correlationGroups)
  - reports (securityReports)
  - marketing (waitlistEntries, blogPosts)
  - hometitle (propertyWatchlistItems, propertySnapshots, propertyChanges)
  - removebrokers (infoBrokers, removalRequests, brokerListings)
- Define all 28 PostgreSQL enums via pgEnum()
- Define all indexes, unique constraints, and foreign keys
- Define all 25 relation definitions via relations() helper
- Update drizzle.config.ts for PostgreSQL dialect
- Update db/index.ts for node-postgres connection
- Replace old placeholder schema.ts with barrel re-export
- Add 38 comprehensive schema tests
This commit is contained in:
2026-05-25 15:35:10 -04:00
parent 9dc55517b1
commit bc20aeaeb6
21 changed files with 1780 additions and 23 deletions

567
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/server/db/schema.ts",
schema: "./src/server/db/schema/index.ts",
out: "./drizzle",
dialect: "sqlite",
driver: "turso",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
authToken: process.env.DATABASE_AUTH_TOKEN,
url: process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/shieldai",
},
});

View File

@@ -17,9 +17,13 @@
"@tailwindcss/vite": "^4.0.0",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@types/three": "^0.184.1",
"@typeschema/valibot": "^0.13.4",
"drizzle-orm": "^0.45.2",
"pg": "^8.21.0",
"solid-js": "^1.9.5",
"tailwindcss": "^4.0.0",
"three": "^0.184.0",
"valibot": "^0.29.0",
"vite": "^7.0.0"
},
@@ -27,6 +31,8 @@
"node": ">=22"
},
"devDependencies": {
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"jsdom": "^29.1.1",
"vite-plugin-solid": "^2.11.12",
"vitest": "^4.1.5"

View File

@@ -1,11 +1,10 @@
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import * as schema from "./schema";
const client = createClient({
url: process.env.DATABASE_URL ?? "file:local.db",
authToken: process.env.DATABASE_AUTH_TOKEN,
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL ?? "postgresql://postgres:postgres@localhost:5432/shieldai",
});
export const db = drizzle(client, { schema });
export const db = drizzle(pool, { schema });

View File

@@ -0,0 +1,442 @@
import { describe, it, expect } from "vitest";
import { getTableConfig } from "drizzle-orm/pg-core";
import * as schema from "./schema";
const tableNames = [
"users", "accounts", "sessions", "deviceTokens",
"familyGroups", "familyGroupMembers", "subscriptions",
"watchlistItems", "exposures",
"alerts",
"voiceEnrollments", "voiceAnalyses", "analysisJobs", "analysisResults",
"spamFeedback", "spamRules",
"auditLogs", "kpiSnapshots",
"normalizedAlerts", "correlationGroups",
"securityReports",
"waitlistEntries", "blogPosts",
"propertyWatchlistItems", "propertySnapshots", "propertyChanges",
"infoBrokers", "removalRequests", "brokerListings",
];
const enumNames = [
"userRole", "deviceType", "platform", "familyMemberRole",
"subscriptionTier", "subscriptionStatus",
"watchlistType", "exposureSource", "exposureSeverity",
"alertType", "alertSeverity", "alertChannel",
"detectionVerdict", "analysisType", "analysisJobStatus",
"feedbackType", "ruleType", "ruleAction",
"alertSource", "alertCategory", "normalizedAlertSeverity", "correlationStatus",
"reportType", "reportStatus",
"propertyChangeType", "propertyChangeSeverity",
"brokerCategory", "removalMethod", "removalStatus",
];
describe("schema exports", () => {
it("exports all 29 tables", () => {
for (const name of tableNames) {
expect((schema as Record<string, unknown>)[name], `Missing table: ${name}`).toBeDefined();
}
});
it("exports all 28 enums", () => {
for (const name of enumNames) {
expect((schema as Record<string, unknown>)[name], `Missing enum: ${name}`).toBeDefined();
}
});
it("exports all 25 relation definitions", () => {
const relationNames = [
"usersRelations", "accountsRelations", "sessionsRelations", "deviceTokensRelations",
"familyGroupsRelations", "familyGroupMembersRelations", "subscriptionsRelations",
"watchlistItemsRelations", "exposuresRelations",
"alertsRelations",
"voiceEnrollmentsRelations", "voiceAnalysesRelations", "analysisJobsRelations", "analysisResultsRelations",
"spamFeedbackRelations", "spamRulesRelations",
"normalizedAlertsRelations", "correlationGroupsRelations",
"securityReportsRelations",
"propertyWatchlistItemsRelations", "propertySnapshotsRelations", "propertyChangesRelations",
"infoBrokersRelations", "removalRequestsRelations", "brokerListingsRelations",
];
for (const name of relationNames) {
expect((schema as Record<string, unknown>)[name], `Missing relation: ${name}`).toBeDefined();
}
});
});
describe("users table", () => {
const config = getTableConfig(schema.users);
it("has expected columns", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("id");
expect(colNames).toContain("email");
expect(colNames).toContain("email_verified");
expect(colNames).toContain("name");
expect(colNames).toContain("image");
expect(colNames).toContain("role");
expect(colNames).toContain("created_at");
expect(colNames).toContain("updated_at");
});
it("has 8 columns", () => {
expect(config.columns).toHaveLength(8);
});
it("has 2 indexes", () => {
expect(config.indexes.length).toBe(2);
});
});
describe("accounts table", () => {
const config = getTableConfig(schema.accounts);
it("has user_id, provider, provider_account_id", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("user_id");
expect(colNames).toContain("provider");
expect(colNames).toContain("provider_account_id");
});
it("has expected columns with correct count", () => {
expect(config.columns.length).toBeGreaterThanOrEqual(9);
});
});
describe("subscriptions table", () => {
const config = getTableConfig(schema.subscriptions);
it("has expected columns with correct count", () => {
expect(config.columns.length).toBeGreaterThanOrEqual(11);
});
it("has stripe_id, tier, status columns", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("stripe_id");
expect(colNames).toContain("tier");
expect(colNames).toContain("status");
expect(colNames).toContain("current_period_start");
expect(colNames).toContain("current_period_end");
});
});
describe("alerts table", () => {
const config = getTableConfig(schema.alerts);
it("has foreign key columns", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("subscription_id");
expect(colNames).toContain("user_id");
expect(colNames).toContain("exposure_id");
});
it("has channel array column", () => {
const channelCol = config.columns.find((c) => c.name === "channel");
expect(channelCol).toBeDefined();
});
});
describe("blogPosts table", () => {
const config = getTableConfig(schema.blogPosts);
it("has tags array column", () => {
const tagsCol = config.columns.find((c) => c.name === "tags");
expect(tagsCol).toBeDefined();
});
it("has all expected columns", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("id");
expect(colNames).toContain("slug");
expect(colNames).toContain("title");
expect(colNames).toContain("content");
expect(colNames).toContain("tags");
expect(colNames).toContain("published");
expect(colNames).toContain("published_at");
expect(colNames).toContain("view_count");
});
});
describe("normalizedAlerts table", () => {
const config = getTableConfig(schema.normalizedAlerts);
it("has source_alert_id column", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("source_alert_id");
});
it("has entities column", () => {
expect(config.columns.find((c) => c.name === "entities")).toBeDefined();
});
});
describe("watchlistItems table", () => {
const config = getTableConfig(schema.watchlistItems);
it("has all expected columns", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("subscription_id");
expect(colNames).toContain("type");
expect(colNames).toContain("value");
expect(colNames).toContain("hash");
expect(colNames).toContain("is_active");
});
});
describe("exposures table", () => {
const config = getTableConfig(schema.exposures);
it("has metadata jsonb, severity, detected_at", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("metadata");
expect(colNames).toContain("severity");
expect(colNames).toContain("detected_at");
expect(colNames).toContain("is_first_time");
});
});
describe("voiceEnrollments table", () => {
const config = getTableConfig(schema.voiceEnrollments);
it("has voice_hash and audio_metadata", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("voice_hash");
expect(colNames).toContain("audio_metadata");
expect(colNames).toContain("is_active");
});
});
describe("analysisJobs table", () => {
const config = getTableConfig(schema.analysisJobs);
it("has analysis_type, status, error_message", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("analysis_type");
expect(colNames).toContain("status");
expect(colNames).toContain("error_message");
expect(colNames).toContain("audio_file_path");
});
});
describe("analysisResults table", () => {
const config = getTableConfig(schema.analysisResults);
it("has analysis_job_id with unique", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("analysis_job_id");
expect(colNames).toContain("synthetic_score");
expect(colNames).toContain("verdict");
expect(colNames).toContain("processing_time_ms");
});
});
describe("spamFeedback table", () => {
const config = getTableConfig(schema.spamFeedback);
it("has phone_number, phone_number_hash, is_spam", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("phone_number");
expect(colNames).toContain("phone_number_hash");
expect(colNames).toContain("is_spam");
expect(colNames).toContain("feedback_type");
});
});
describe("spamRules table", () => {
const config = getTableConfig(schema.spamRules);
it("has rule_type, pattern, action", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("rule_type");
expect(colNames).toContain("pattern");
expect(colNames).toContain("action");
expect(colNames).toContain("is_global");
});
});
describe("auditLogs table", () => {
const config = getTableConfig(schema.auditLogs);
it("has action, resource, resource_id", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("action");
expect(colNames).toContain("resource");
expect(colNames).toContain("resource_id");
expect(colNames).toContain("ip_address");
expect(colNames).toContain("user_agent");
});
});
describe("kpiSnapshots table", () => {
const config = getTableConfig(schema.kpiSnapshots);
it("has date, metric_name, metric_value", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("date");
expect(colNames).toContain("metric_name");
expect(colNames).toContain("metric_value");
});
});
describe("propertyWatchlistItems table", () => {
const config = getTableConfig(schema.propertyWatchlistItems);
it("has address, street_address, parcel_id", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("address");
expect(colNames).toContain("street_address");
expect(colNames).toContain("parcel_id");
expect(colNames).toContain("latitude");
expect(colNames).toContain("longitude");
});
});
describe("propertySnapshots table", () => {
const config = getTableConfig(schema.propertySnapshots);
it("has captured_at, owner_name, address jsonb", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("captured_at");
expect(colNames).toContain("owner_name");
expect(colNames).toContain("address");
expect(colNames).toContain("property_type");
expect(colNames).toContain("tax_amount");
expect(colNames).toContain("lien_count");
});
});
describe("propertyChanges table", () => {
const config = getTableConfig(schema.propertyChanges);
it("has change_type, severity, details", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("change_type");
expect(colNames).toContain("severity");
expect(colNames).toContain("details");
expect(colNames).toContain("detected_at");
});
});
describe("infoBrokers table", () => {
const config = getTableConfig(schema.infoBrokers);
it("has name, domain, category", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("name");
expect(colNames).toContain("domain");
expect(colNames).toContain("category");
expect(colNames).toContain("removal_method");
expect(colNames).toContain("estimated_days");
});
});
describe("removalRequests table", () => {
const config = getTableConfig(schema.removalRequests);
it("has personal_info jsonb, status, attempts", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("personal_info");
expect(colNames).toContain("status");
expect(colNames).toContain("attempts");
expect(colNames).toContain("next_retry_at");
expect(colNames).toContain("error");
expect(colNames).toContain("notes");
});
});
describe("brokerListings table", () => {
const config = getTableConfig(schema.brokerListings);
it("has url, data_found, screenshot_url", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("url");
expect(colNames).toContain("data_found");
expect(colNames).toContain("screenshot_url");
expect(colNames).toContain("is_removed");
});
});
describe("correlationGroups table", () => {
const config = getTableConfig(schema.correlationGroups);
it("has entities, highest_severity, alert_count", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("entities");
expect(colNames).toContain("highest_severity");
expect(colNames).toContain("alert_count");
expect(colNames).toContain("summary");
});
});
describe("securityReports table", () => {
const config = getTableConfig(schema.securityReports);
it("has period_start, period_end, pdf_url", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("period_start");
expect(colNames).toContain("period_end");
expect(colNames).toContain("pdf_url");
expect(colNames).toContain("html_content");
expect(colNames).toContain("report_type");
expect(colNames).toContain("status");
});
});
describe("waitlistEntries table", () => {
const config = getTableConfig(schema.waitlistEntries);
it("has email, source, utm tracking, conversion tracking", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("email");
expect(colNames).toContain("source");
expect(colNames).toContain("utm_source");
expect(colNames).toContain("utm_medium");
expect(colNames).toContain("utm_campaign");
expect(colNames).toContain("converted_at");
expect(colNames).toContain("converted_to_user_id");
});
});
describe("familyGroups table", () => {
const config = getTableConfig(schema.familyGroups);
it("has owner_id, name", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("owner_id");
expect(colNames).toContain("name");
});
});
describe("familyGroupMembers table", () => {
const config = getTableConfig(schema.familyGroupMembers);
it("has group_id, user_id, role, joined_at", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("group_id");
expect(colNames).toContain("user_id");
expect(colNames).toContain("role");
expect(colNames).toContain("joined_at");
});
});
describe("sessions table", () => {
const config = getTableConfig(schema.sessions);
it("has session_token, expires", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("session_token");
expect(colNames).toContain("expires");
expect(colNames).toContain("user_id");
});
});
describe("deviceTokens table", () => {
const config = getTableConfig(schema.deviceTokens);
it("has device_type, platform, token, is_active", () => {
const colNames = config.columns.map((c) => c.name);
expect(colNames).toContain("device_type");
expect(colNames).toContain("platform");
expect(colNames).toContain("token");
expect(colNames).toContain("is_active");
expect(colNames).toContain("last_used_at");
});
});

View File

@@ -1,10 +1 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
export * from "./schema/index";

View File

@@ -0,0 +1,26 @@
import { pgTable, text, timestamp, index, uuid, boolean } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { subscriptions } from "./subscription";
import { exposures } from "./darkwatch";
import { alertType, alertSeverity, alertChannel } from "./enums";
export const alerts = pgTable("alerts", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
exposureId: uuid("exposure_id").references(() => exposures.id),
type: alertType("type").notNull(),
title: text("title").notNull(),
message: text("message").notNull(),
severity: alertSeverity("severity").default("info").notNull(),
isRead: boolean("is_read").default(false).notNull(),
readAt: timestamp("read_at", { withTimezone: true, mode: "date" }),
channel: alertChannel("channel").array().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subscriptionIdIdx: index("alerts_subscription_id_idx").on(table.subscriptionId),
userIdIdx: index("alerts_user_id_idx").on(table.userId),
isReadIdx: index("alerts_is_read_idx").on(table.isRead),
createdAtIdx: index("alerts_created_at_idx").on(table.createdAt),
}));

View File

@@ -0,0 +1,31 @@
import { pgTable, text, timestamp, index, uuid, jsonb, doublePrecision } from "drizzle-orm/pg-core";
export const auditLogs = pgTable("audit_logs", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id"),
action: text("action").notNull(),
resource: text("resource").notNull(),
resourceId: text("resource_id"),
changes: jsonb("changes"),
metadata: jsonb("metadata"),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
userIdIdx: index("audit_logs_user_id_idx").on(table.userId),
actionIdx: index("audit_logs_action_idx").on(table.action),
resourceIdx: index("audit_logs_resource_idx").on(table.resource),
createdAtIdx: index("audit_logs_created_at_idx").on(table.createdAt),
}));
export const kpiSnapshots = pgTable("kpi_snapshots", {
id: uuid("id").defaultRandom().primaryKey(),
date: timestamp("date", { withTimezone: true, mode: "date" }).notNull().unique(),
metricName: text("metric_name").notNull(),
metricValue: doublePrecision("metric_value").notNull(),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
metricNameIdx: index("kpi_snapshots_metric_name_idx").on(table.metricName),
dateIdx: index("kpi_snapshots_date_idx").on(table.date),
}));

View File

@@ -0,0 +1,66 @@
import { pgTable, text, timestamp, uniqueIndex, index, uuid, integer, boolean } from "drizzle-orm/pg-core";
import { userRole, deviceType, platform } from "./enums";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull().unique(),
emailVerified: timestamp("email_verified", { withTimezone: true, mode: "date" }),
name: text("name"),
image: text("image"),
role: userRole("role").default("user").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
emailIdx: index("users_email_idx").on(table.email),
roleIdx: index("users_role_idx").on(table.role),
}));
export const accounts = pgTable("accounts", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
expiresAt: integer("expires_at"),
tokenType: text("token_type"),
scope: text("scope"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userProviderUnique: uniqueIndex("accounts_user_provider_unique").on(table.userId, table.provider, table.providerAccountId),
userIdIdx: index("accounts_user_id_idx").on(table.userId),
}));
export const sessions = pgTable("sessions", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
sessionToken: text("session_token").notNull().unique(),
expires: timestamp("expires", { withTimezone: true, mode: "date" }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
sessionTokenIdx: index("sessions_session_token_idx").on(table.sessionToken),
userIdIdx: index("sessions_user_id_idx").on(table.userId),
}));
export const deviceTokens = pgTable("device_tokens", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
deviceType: deviceType("device_type").notNull(),
token: text("token").notNull().unique(),
platform: platform("platform").notNull(),
appName: text("app_name"),
appVersion: text("app_version"),
osVersion: text("os_version"),
model: text("model"),
isActive: boolean("is_active").default(true).notNull(),
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("device_tokens_user_id_idx").on(table.userId),
deviceTypeIdx: index("device_tokens_device_type_idx").on(table.deviceType),
platformIdx: index("device_tokens_platform_idx").on(table.platform),
isActiveIdx: index("device_tokens_is_active_idx").on(table.isActive),
}));

View File

@@ -0,0 +1,45 @@
import { pgTable, text, timestamp, uniqueIndex, index, uuid, jsonb, integer } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { alertSource, alertCategory, normalizedAlertSeverity, correlationStatus } from "./enums";
export const correlationGroups = pgTable("correlation_groups", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
entities: jsonb("entities").notNull(),
highestSeverity: normalizedAlertSeverity("highest_severity").notNull(),
status: correlationStatus("status").default("ACTIVE").notNull(),
alertCount: integer("alert_count").default(0).notNull(),
summary: text("summary"),
resolvedAt: timestamp("resolved_at", { withTimezone: true, mode: "date" }),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("correlation_groups_user_id_idx").on(table.userId),
statusIdx: index("correlation_groups_status_idx").on(table.status),
userIdStatusIdx: index("correlation_groups_user_id_status_idx").on(table.userId, table.status),
createdAtIdx: index("correlation_groups_created_at_idx").on(table.createdAt),
}));
export const normalizedAlerts = pgTable("normalized_alerts", {
id: uuid("id").defaultRandom().primaryKey(),
source: alertSource("source").notNull(),
category: alertCategory("category").notNull(),
severity: normalizedAlertSeverity("severity").notNull(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
title: text("title").notNull(),
description: text("description").notNull(),
entities: jsonb("entities").notNull(),
sourceAlertId: text("source_alert_id").notNull(),
groupId: uuid("group_id").references(() => correlationGroups.id),
payload: jsonb("payload"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
sourceAlertIdUnique: uniqueIndex("normalized_alerts_source_alert_id_unique").on(table.sourceAlertId),
userIdIdx: index("normalized_alerts_user_id_idx").on(table.userId),
groupIdIdx: index("normalized_alerts_group_id_idx").on(table.groupId),
sourceIdx: index("normalized_alerts_source_idx").on(table.source),
severityIdx: index("normalized_alerts_severity_idx").on(table.severity),
createdAtIdx: index("normalized_alerts_created_at_idx").on(table.createdAt),
userIdCreatedAtIdx: index("normalized_alerts_user_id_created_at_idx").on(table.userId, table.createdAt),
}));

View File

@@ -0,0 +1,41 @@
import { pgTable, text, timestamp, uniqueIndex, index, uuid, boolean, jsonb } from "drizzle-orm/pg-core";
import { subscriptions } from "./subscription";
import { watchlistType, exposureSource, exposureSeverity } from "./enums";
export const watchlistItems = pgTable("watchlist_items", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
type: watchlistType("type").notNull(),
value: text("value").notNull(),
hash: text("hash").notNull(),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subTypeHashUnique: uniqueIndex("watchlist_items_sub_type_hash_unique").on(table.subscriptionId, table.type, table.hash),
subscriptionIdIdx: index("watchlist_items_subscription_id_idx").on(table.subscriptionId),
typeIdx: index("watchlist_items_type_idx").on(table.type),
hashIdx: index("watchlist_items_hash_idx").on(table.hash),
}));
export const exposures = pgTable("exposures", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
watchlistItemId: uuid("watchlist_item_id").references(() => watchlistItems.id),
source: exposureSource("source").notNull(),
dataType: watchlistType("data_type").notNull(),
identifier: text("identifier").notNull(),
identifierHash: text("identifier_hash").notNull(),
severity: exposureSeverity("severity").default("info").notNull(),
metadata: jsonb("metadata"),
isFirstTime: boolean("is_first_time").default(false).notNull(),
detectedAt: timestamp("detected_at", { withTimezone: true, mode: "date" }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subscriptionIdIdx: index("exposures_subscription_id_idx").on(table.subscriptionId),
watchlistItemIdIdx: index("exposures_watchlist_item_id_idx").on(table.watchlistItemId),
sourceIdx: index("exposures_source_idx").on(table.source),
severityIdx: index("exposures_severity_idx").on(table.severity),
detectedAtIdx: index("exposures_detected_at_idx").on(table.detectedAt),
}));

View File

@@ -0,0 +1,31 @@
import { pgEnum } from "drizzle-orm/pg-core";
export const userRole = pgEnum("user_role", ["user", "family_admin", "family_member", "support"]);
export const deviceType = pgEnum("device_type", ["mobile", "web", "desktop"]);
export const platform = pgEnum("platform", ["ios", "android", "web"]);
export const familyMemberRole = pgEnum("family_member_role", ["owner", "admin", "member"]);
export const subscriptionTier = pgEnum("subscription_tier", ["basic", "plus", "premium"]);
export const subscriptionStatus = pgEnum("subscription_status", ["active", "past_due", "canceled", "unpaid", "trialing"]);
export const watchlistType = pgEnum("watchlist_type", ["email", "phoneNumber", "ssn", "address", "domain"]);
export const exposureSource = pgEnum("exposure_source", ["hibp", "securityTrails", "censys", "darkWebForum", "shodan", "honeypot"]);
export const exposureSeverity = pgEnum("exposure_severity", ["info", "warning", "critical"]);
export const alertType = pgEnum("alert_type", ["exposure_detected", "exposure_resolved", "scan_complete", "subscription_changed", "system_warning"]);
export const alertSeverity = pgEnum("alert_severity", ["info", "warning", "critical"]);
export const alertChannel = pgEnum("alert_channel", ["email", "push", "sms"]);
export const detectionVerdict = pgEnum("detection_verdict", ["NATURAL", "SYNTHETIC", "UNCERTAIN"]);
export const analysisType = pgEnum("analysis_type", ["SYNTHETIC_DETECTION", "VOICE_MATCH", "BATCH"]);
export const analysisJobStatus = pgEnum("analysis_job_status", ["PENDING", "RUNNING", "COMPLETED", "FAILED"]);
export const feedbackType = pgEnum("feedback_type", ["initial_detection", "user_confirmation", "user_rejection", "auto_learned"]);
export const ruleType = pgEnum("rule_type", ["phoneNumber", "areaCode", "prefix", "pattern", "reputation"]);
export const ruleAction = pgEnum("rule_action", ["block", "flag", "allow", "challenge"]);
export const alertSource = pgEnum("alert_source", ["DARKWATCH", "SPAMSHIELD", "VOICEPRINT", "CALL_ANALYSIS", "HOME_TITLE", "INFO_BROKER"]);
export const alertCategory = pgEnum("alert_category", ["BREACH_EXPOSURE", "SPAM_CALL", "SPAM_SMS", "SYNTHETIC_VOICE", "VOICE_MISMATCH", "CALL_ANOMALY", "CALL_QUALITY", "CALL_EVENT", "HOME_TITLE", "INFO_BROKER_LISTING", "INFO_BROKER_REMOVAL"]);
export const normalizedAlertSeverity = pgEnum("normalized_alert_severity", ["LOW", "INFO", "MEDIUM", "WARNING", "HIGH", "CRITICAL"]);
export const correlationStatus = pgEnum("correlation_status", ["ACTIVE", "RESOLVED", "FALSE_POSITIVE"]);
export const reportType = pgEnum("report_type", ["MONTHLY_PLUS", "ANNUAL_PREMIUM", "WEEKLY_DIGEST"]);
export const reportStatus = pgEnum("report_status", ["PENDING", "GENERATING", "COMPLETED", "FAILED", "DELIVERED"]);
export const propertyChangeType = pgEnum("property_change_type", ["tax_change", "deed_change", "ownership_transfer", "lien_filing", "metadata_change"]);
export const propertyChangeSeverity = pgEnum("property_change_severity", ["info", "warning", "critical"]);
export const brokerCategory = pgEnum("broker_category", ["PEOPLE_SEARCH", "BACKGROUND_CHECK", "PUBLIC_RECORDS", "REVERSE_LOOKUP", "SOCIAL_MEDIA"]);
export const removalMethod = pgEnum("removal_method", ["AUTOMATED", "MANUAL_FORM", "EMAIL", "PHONE", "MAIL", "NONE"]);
export const removalStatus = pgEnum("removal_status", ["PENDING", "SUBMITTED", "IN_PROGRESS", "COMPLETED", "FAILED", "REJECTED", "CANCELLED"]);

View File

@@ -0,0 +1,59 @@
import { pgTable, text, timestamp, uniqueIndex, index, uuid, boolean, jsonb, doublePrecision, integer } from "drizzle-orm/pg-core";
import { subscriptions } from "./subscription";
import { propertyChangeType, propertyChangeSeverity } from "./enums";
export const propertyWatchlistItems = pgTable("property_watchlist_items", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
address: text("address").notNull(),
parcelId: text("parcel_id"),
ownerName: text("owner_name"),
streetAddress: text("street_address").notNull(),
city: text("city").default(""),
state: text("state").default(""),
zipCode: text("zip_code").default(""),
latitude: doublePrecision("latitude"),
longitude: doublePrecision("longitude"),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subParcelIdUnique: uniqueIndex("property_watchlist_items_sub_parcel_unique").on(table.subscriptionId, table.parcelId),
subscriptionIdIdx: index("property_watchlist_items_subscription_id_idx").on(table.subscriptionId),
parcelIdIdx: index("property_watchlist_items_parcel_id_idx").on(table.parcelId),
addressIdx: index("property_watchlist_items_address_idx").on(table.address),
}));
export const propertySnapshots = pgTable("property_snapshots", {
id: uuid("id").defaultRandom().primaryKey(),
propertyWatchlistItemId: uuid("property_watchlist_item_id").notNull().references(() => propertyWatchlistItems.id, { onDelete: "cascade" }),
subscriptionId: uuid("subscription_id").notNull(),
capturedAt: timestamp("captured_at", { withTimezone: true, mode: "date" }).notNull(),
ownerName: text("owner_name").notNull(),
address: jsonb("address").notNull(),
deedDate: text("deed_date"),
taxId: text("tax_id"),
propertyType: text("property_type").default("residential").notNull(),
taxAmount: doublePrecision("tax_amount"),
lienCount: integer("lien_count").default(0).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
propertyWatchlistItemIdIdx: index("property_snapshots_property_watchlist_item_id_idx").on(table.propertyWatchlistItemId),
subscriptionIdIdx: index("property_snapshots_subscription_id_idx").on(table.subscriptionId),
capturedAtIdx: index("property_snapshots_captured_at_idx").on(table.capturedAt),
}));
export const propertyChanges = pgTable("property_changes", {
id: uuid("id").defaultRandom().primaryKey(),
propertyWatchlistItemId: uuid("property_watchlist_item_id").notNull().references(() => propertyWatchlistItems.id, { onDelete: "cascade" }),
snapshotId: uuid("snapshot_id").references(() => propertySnapshots.id),
changeType: propertyChangeType("change_type").notNull(),
severity: propertyChangeSeverity("severity").default("info").notNull(),
details: jsonb("details"),
detectedAt: timestamp("detected_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
propertyWatchlistItemIdIdx: index("property_changes_property_watchlist_item_id_idx").on(table.propertyWatchlistItemId),
snapshotIdIdx: index("property_changes_snapshot_id_idx").on(table.snapshotId),
changeTypeIdx: index("property_changes_change_type_idx").on(table.changeType),
}));

View File

@@ -0,0 +1,14 @@
export * from "./enums";
export * from "./auth";
export * from "./subscription";
export * from "./darkwatch";
export * from "./alerts";
export * from "./voiceprint";
export * from "./spamshield";
export * from "./audit";
export * from "./correlation";
export * from "./reports";
export * from "./marketing";
export * from "./hometitle";
export * from "./removebrokers";
export * from "./relations";

View File

@@ -0,0 +1,43 @@
import { pgTable, text, timestamp, index, uuid, boolean, integer, jsonb } from "drizzle-orm/pg-core";
import { subscriptionTier } from "./enums";
export const waitlistEntries = pgTable("waitlist_entries", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull(),
name: text("name"),
source: text("source"),
tier: subscriptionTier("tier"),
utmSource: text("utm_source"),
utmMedium: text("utm_medium"),
utmCampaign: text("utm_campaign"),
metadata: jsonb("metadata"),
convertedAt: timestamp("converted_at", { withTimezone: true, mode: "date" }),
convertedToUserId: text("converted_to_user_id"),
convertedToSubscriptionId: text("converted_to_subscription_id"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
emailIdx: index("waitlist_entries_email_idx").on(table.email),
sourceIdx: index("waitlist_entries_source_idx").on(table.source),
createdAtIdx: index("waitlist_entries_created_at_idx").on(table.createdAt),
}));
export const blogPosts = pgTable("blog_posts", {
id: uuid("id").defaultRandom().primaryKey(),
slug: text("slug").notNull().unique(),
title: text("title").notNull(),
excerpt: text("excerpt"),
content: text("content").notNull(),
authorName: text("author_name"),
coverImageUrl: text("cover_image_url"),
tags: text("tags").array().notNull(),
published: boolean("published").default(false).notNull(),
publishedAt: timestamp("published_at", { withTimezone: true, mode: "date" }),
viewCount: integer("view_count").default(0).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
slugIdx: index("blog_posts_slug_idx").on(table.slug),
publishedIdx: index("blog_posts_published_idx").on(table.published, table.publishedAt),
tagsIdx: index("blog_posts_tags_idx").on(table.tags),
}));

View File

@@ -0,0 +1,156 @@
import { relations } from "drizzle-orm";
import { users } from "./auth";
import { accounts } from "./auth";
import { sessions } from "./auth";
import { deviceTokens } from "./auth";
import { familyGroups, familyGroupMembers, subscriptions } from "./subscription";
import { watchlistItems, exposures } from "./darkwatch";
import { alerts } from "./alerts";
import { voiceEnrollments, voiceAnalyses, analysisJobs, analysisResults } from "./voiceprint";
import { spamFeedback, spamRules } from "./spamshield";
import { normalizedAlerts, correlationGroups } from "./correlation";
import { securityReports } from "./reports";
import { propertyWatchlistItems, propertySnapshots, propertyChanges } from "./hometitle";
import { infoBrokers, removalRequests, brokerListings } from "./removebrokers";
export const usersRelations = relations(users, ({ many }) => ({
accounts: many(accounts),
sessions: many(sessions),
deviceTokens: many(deviceTokens),
familyGroups: many(familyGroupMembers),
familyGroupOwned: many(familyGroups),
subscriptions: many(subscriptions),
alerts: many(alerts),
voiceEnrollments: many(voiceEnrollments),
voiceAnalyses: many(voiceAnalyses),
spamFeedback: many(spamFeedback),
spamRules: many(spamRules),
normalizedAlerts: many(normalizedAlerts),
correlationGroups: many(correlationGroups),
securityReports: many(securityReports),
analysisJobs: many(analysisJobs),
}));
export const accountsRelations = relations(accounts, ({ one }) => ({
user: one(users, { fields: [accounts.userId], references: [users.id] }),
}));
export const sessionsRelations = relations(sessions, ({ one }) => ({
user: one(users, { fields: [sessions.userId], references: [users.id] }),
}));
export const deviceTokensRelations = relations(deviceTokens, ({ one }) => ({
user: one(users, { fields: [deviceTokens.userId], references: [users.id] }),
}));
export const familyGroupsRelations = relations(familyGroups, ({ one, many }) => ({
owner: one(users, { fields: [familyGroups.ownerId], references: [users.id] }),
members: many(familyGroupMembers),
subscriptions: many(subscriptions),
}));
export const familyGroupMembersRelations = relations(familyGroupMembers, ({ one }) => ({
group: one(familyGroups, { fields: [familyGroupMembers.groupId], references: [familyGroups.id] }),
user: one(users, { fields: [familyGroupMembers.userId], references: [users.id] }),
}));
export const subscriptionsRelations = relations(subscriptions, ({ one, many }) => ({
user: one(users, { fields: [subscriptions.userId], references: [users.id] }),
familyGroup: one(familyGroups, { fields: [subscriptions.familyGroupId], references: [familyGroups.id] }),
watchlistItems: many(watchlistItems),
exposures: many(exposures),
alerts: many(alerts),
propertyWatchlistItems: many(propertyWatchlistItems),
removalRequests: many(removalRequests),
brokerListings: many(brokerListings),
}));
export const watchlistItemsRelations = relations(watchlistItems, ({ one, many }) => ({
subscription: one(subscriptions, { fields: [watchlistItems.subscriptionId], references: [subscriptions.id] }),
exposures: many(exposures),
}));
export const exposuresRelations = relations(exposures, ({ one, many }) => ({
subscription: one(subscriptions, { fields: [exposures.subscriptionId], references: [subscriptions.id] }),
watchlistItem: one(watchlistItems, { fields: [exposures.watchlistItemId], references: [watchlistItems.id] }),
alerts: many(alerts),
}));
export const alertsRelations = relations(alerts, ({ one }) => ({
subscription: one(subscriptions, { fields: [alerts.subscriptionId], references: [subscriptions.id] }),
user: one(users, { fields: [alerts.userId], references: [users.id] }),
exposure: one(exposures, { fields: [alerts.exposureId], references: [exposures.id] }),
}));
export const voiceEnrollmentsRelations = relations(voiceEnrollments, ({ one, many }) => ({
user: one(users, { fields: [voiceEnrollments.userId], references: [users.id] }),
analyses: many(voiceAnalyses),
}));
export const voiceAnalysesRelations = relations(voiceAnalyses, ({ one }) => ({
enrollment: one(voiceEnrollments, { fields: [voiceAnalyses.enrollmentId], references: [voiceEnrollments.id] }),
user: one(users, { fields: [voiceAnalyses.userId], references: [users.id] }),
}));
export const analysisJobsRelations = relations(analysisJobs, ({ one }) => ({
user: one(users, { fields: [analysisJobs.userId], references: [users.id] }),
result: one(analysisResults),
}));
export const analysisResultsRelations = relations(analysisResults, ({ one }) => ({
analysisJob: one(analysisJobs, { fields: [analysisResults.analysisJobId], references: [analysisJobs.id] }),
}));
export const spamFeedbackRelations = relations(spamFeedback, ({ one }) => ({
user: one(users, { fields: [spamFeedback.userId], references: [users.id] }),
}));
export const spamRulesRelations = relations(spamRules, ({ one }) => ({
user: one(users, { fields: [spamRules.userId], references: [users.id] }),
}));
export const normalizedAlertsRelations = relations(normalizedAlerts, ({ one }) => ({
correlationGroup: one(correlationGroups, { fields: [normalizedAlerts.groupId], references: [correlationGroups.id] }),
user: one(users, { fields: [normalizedAlerts.userId], references: [users.id] }),
}));
export const correlationGroupsRelations = relations(correlationGroups, ({ one, many }) => ({
user: one(users, { fields: [correlationGroups.userId], references: [users.id] }),
alerts: many(normalizedAlerts),
}));
export const securityReportsRelations = relations(securityReports, ({ one }) => ({
user: one(users, { fields: [securityReports.userId], references: [users.id] }),
}));
export const propertyWatchlistItemsRelations = relations(propertyWatchlistItems, ({ one, many }) => ({
subscription: one(subscriptions, { fields: [propertyWatchlistItems.subscriptionId], references: [subscriptions.id] }),
snapshots: many(propertySnapshots),
changes: many(propertyChanges),
}));
export const propertySnapshotsRelations = relations(propertySnapshots, ({ one, many }) => ({
propertyWatchlistItem: one(propertyWatchlistItems, { fields: [propertySnapshots.propertyWatchlistItemId], references: [propertyWatchlistItems.id] }),
changes: many(propertyChanges),
}));
export const propertyChangesRelations = relations(propertyChanges, ({ one }) => ({
propertyWatchlistItem: one(propertyWatchlistItems, { fields: [propertyChanges.propertyWatchlistItemId], references: [propertyWatchlistItems.id] }),
snapshot: one(propertySnapshots, { fields: [propertyChanges.snapshotId], references: [propertySnapshots.id] }),
}));
export const infoBrokersRelations = relations(infoBrokers, ({ many }) => ({
removalRequests: many(removalRequests),
}));
export const removalRequestsRelations = relations(removalRequests, ({ one, many }) => ({
broker: one(infoBrokers, { fields: [removalRequests.brokerId], references: [infoBrokers.id] }),
subscription: one(subscriptions, { fields: [removalRequests.subscriptionId], references: [subscriptions.id] }),
brokerListings: many(brokerListings),
}));
export const brokerListingsRelations = relations(brokerListings, ({ one }) => ({
removalRequest: one(removalRequests, { fields: [brokerListings.removalRequestId], references: [removalRequests.id] }),
subscription: one(subscriptions, { fields: [brokerListings.subscriptionId], references: [subscriptions.id] }),
}));

View File

@@ -0,0 +1,67 @@
import { pgTable, text, timestamp, index, uuid, boolean, jsonb, integer } from "drizzle-orm/pg-core";
import { subscriptions } from "./subscription";
import { brokerCategory, removalMethod, removalStatus } from "./enums";
export const infoBrokers = pgTable("info_brokers", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
domain: text("domain").notNull().unique(),
category: brokerCategory("category").notNull(),
removalMethod: removalMethod("removal_method").notNull(),
removalUrl: text("removal_url"),
requiresAccount: boolean("requires_account").default(false).notNull(),
requiresVerification: boolean("requires_verification").default(false).notNull(),
estimatedDays: integer("estimated_days").default(14).notNull(),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
categoryIdx: index("info_brokers_category_idx").on(table.category),
isActiveIdx: index("info_brokers_is_active_idx").on(table.isActive),
removalMethodIdx: index("info_brokers_removal_method_idx").on(table.removalMethod),
}));
export const removalRequests = pgTable("removal_requests", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
brokerId: uuid("broker_id").notNull().references(() => infoBrokers.id),
status: removalStatus("status").default("PENDING").notNull(),
personalInfo: jsonb("personal_info").notNull(),
method: removalMethod("method").notNull(),
attempts: integer("attempts").default(0).notNull(),
nextRetryAt: timestamp("next_retry_at", { withTimezone: true, mode: "date" }),
submittedAt: timestamp("submitted_at", { withTimezone: true, mode: "date" }),
completedAt: timestamp("completed_at", { withTimezone: true, mode: "date" }),
error: text("error"),
notes: text("notes"),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subscriptionIdIdx: index("removal_requests_subscription_id_idx").on(table.subscriptionId),
brokerIdIdx: index("removal_requests_broker_id_idx").on(table.brokerId),
statusIdx: index("removal_requests_status_idx").on(table.status),
submittedAtIdx: index("removal_requests_submitted_at_idx").on(table.submittedAt),
subscriptionIdStatusIdx: index("removal_requests_sub_id_status_idx").on(table.subscriptionId, table.status),
}));
export const brokerListings = pgTable("broker_listings", {
id: uuid("id").defaultRandom().primaryKey(),
subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
brokerId: uuid("broker_id").notNull(),
removalRequestId: uuid("removal_request_id").references(() => removalRequests.id),
url: text("url").notNull(),
dataFound: jsonb("data_found").notNull(),
screenshotUrl: text("screenshot_url"),
isRemoved: boolean("is_removed").default(false).notNull(),
removedAt: timestamp("removed_at", { withTimezone: true, mode: "date" }),
scannedAt: timestamp("scanned_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
subscriptionIdIdx: index("broker_listings_subscription_id_idx").on(table.subscriptionId),
brokerIdIdx: index("broker_listings_broker_id_idx").on(table.brokerId),
removalRequestIdIdx: index("broker_listings_removal_request_id_idx").on(table.removalRequestId),
isRemovedIdx: index("broker_listings_is_removed_idx").on(table.isRemoved),
subscriptionIdIsRemovedIdx: index("broker_listings_sub_id_is_removed_idx").on(table.subscriptionId, table.isRemoved),
}));

View File

@@ -0,0 +1,30 @@
import { pgTable, text, timestamp, index, uuid, jsonb } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { reportType, reportStatus } from "./enums";
export const securityReports = pgTable("security_reports", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
subscriptionId: uuid("subscription_id").notNull(),
reportType: reportType("report_type").notNull(),
status: reportStatus("status").default("PENDING").notNull(),
periodStart: timestamp("period_start", { withTimezone: true, mode: "date" }).notNull(),
periodEnd: timestamp("period_end", { withTimezone: true, mode: "date" }).notNull(),
title: text("title").notNull(),
summary: text("summary"),
htmlContent: text("html_content"),
pdfUrl: text("pdf_url"),
dataPayload: jsonb("data_payload"),
error: text("error"),
scheduledFor: timestamp("scheduled_for", { withTimezone: true, mode: "date" }),
deliveredAt: timestamp("delivered_at", { withTimezone: true, mode: "date" }),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("security_reports_user_id_idx").on(table.userId),
subscriptionIdIdx: index("security_reports_subscription_id_idx").on(table.subscriptionId),
reportTypeIdx: index("security_reports_report_type_idx").on(table.reportType),
statusIdx: index("security_reports_status_idx").on(table.status),
periodIdx: index("security_reports_period_idx").on(table.periodStart, table.periodEnd),
createdAtIdx: index("security_reports_created_at_idx").on(table.createdAt),
}));

View File

@@ -0,0 +1,37 @@
import { pgTable, text, timestamp, index, uuid, boolean, jsonb, doublePrecision, integer } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { feedbackType, ruleType, ruleAction } from "./enums";
export const spamFeedback = pgTable("spam_feedback", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
phoneNumber: text("phone_number").notNull(),
phoneNumberHash: text("phone_number_hash").notNull(),
isSpam: boolean("is_spam").notNull(),
confidence: doublePrecision("confidence"),
feedbackType: feedbackType("feedback_type").notNull(),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("spam_feedback_user_id_idx").on(table.userId),
phoneNumberHashIdx: index("spam_feedback_phone_number_hash_idx").on(table.phoneNumberHash),
isSpamIdx: index("spam_feedback_is_spam_idx").on(table.isSpam),
}));
export const spamRules = pgTable("spam_rules", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }),
isGlobal: boolean("is_global").default(false).notNull(),
ruleType: ruleType("rule_type").notNull(),
pattern: text("pattern").notNull(),
action: ruleAction("action").notNull(),
priority: integer("priority").default(0).notNull(),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("spam_rules_user_id_idx").on(table.userId),
isGlobalIdx: index("spam_rules_is_global_idx").on(table.isGlobal),
ruleTypeIdx: index("spam_rules_rule_type_idx").on(table.ruleType),
}));

View File

@@ -0,0 +1,47 @@
import { pgTable, text, timestamp, uniqueIndex, index, uuid, boolean } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { familyMemberRole, subscriptionTier, subscriptionStatus } from "./enums";
export const familyGroups = pgTable("family_groups", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
ownerId: uuid("owner_id").notNull().references(() => users.id),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
ownerIdIdx: index("family_groups_owner_id_idx").on(table.ownerId),
nameIdx: index("family_groups_name_idx").on(table.name),
}));
export const familyGroupMembers = pgTable("family_group_members", {
id: uuid("id").defaultRandom().primaryKey(),
groupId: uuid("group_id").notNull().references(() => familyGroups.id, { onDelete: "cascade" }),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
role: familyMemberRole("role").default("member").notNull(),
joinedAt: timestamp("joined_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
groupUserUnique: uniqueIndex("family_group_members_group_user_unique").on(table.groupId, table.userId),
groupIdIdx: index("family_group_members_group_id_idx").on(table.groupId),
userIdIdx: index("family_group_members_user_id_idx").on(table.userId),
}));
export const subscriptions = pgTable("subscriptions", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
familyGroupId: uuid("family_group_id").references(() => familyGroups.id),
stripeId: text("stripe_id").unique(),
tier: subscriptionTier("tier").default("basic").notNull(),
status: subscriptionStatus("status").default("active").notNull(),
currentPeriodStart: timestamp("current_period_start", { withTimezone: true, mode: "date" }).notNull(),
currentPeriodEnd: timestamp("current_period_end", { withTimezone: true, mode: "date" }).notNull(),
cancelAtPeriodEnd: boolean("cancel_at_period_end").default(false).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("subscriptions_user_id_idx").on(table.userId),
familyGroupIdIdx: index("subscriptions_family_group_id_idx").on(table.familyGroupId),
stripeIdIdx: index("subscriptions_stripe_id_idx").on(table.stripeId),
tierIdx: index("subscriptions_tier_idx").on(table.tier),
}));

View File

@@ -0,0 +1,65 @@
import { pgTable, text, timestamp, index, uuid, boolean, jsonb, doublePrecision, integer } from "drizzle-orm/pg-core";
import { users } from "./auth";
import { detectionVerdict, analysisType, analysisJobStatus } from "./enums";
export const voiceEnrollments = pgTable("voice_enrollments", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
voiceHash: text("voice_hash").notNull(),
audioMetadata: jsonb("audio_metadata"),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).defaultNow().notNull().$onUpdate(() => new Date()),
}, (table) => ({
userIdIdx: index("voice_enrollments_user_id_idx").on(table.userId),
voiceHashIdx: index("voice_enrollments_voice_hash_idx").on(table.voiceHash),
}));
export const voiceAnalyses = pgTable("voice_analyses", {
id: uuid("id").defaultRandom().primaryKey(),
enrollmentId: uuid("enrollment_id").references(() => voiceEnrollments.id),
userId: uuid("user_id").notNull().references(() => users.id),
audioHash: text("audio_hash").notNull(),
isSynthetic: boolean("is_synthetic").notNull(),
confidence: doublePrecision("confidence").notNull(),
analysisResult: jsonb("analysis_result").notNull(),
audioUrl: text("audio_url").notNull(),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
userIdIdx: index("voice_analyses_user_id_idx").on(table.userId),
enrollmentIdIdx: index("voice_analyses_enrollment_id_idx").on(table.enrollmentId),
audioHashIdx: index("voice_analyses_audio_hash_idx").on(table.audioHash),
}));
export const analysisJobs = pgTable("analysis_jobs", {
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id").notNull().references(() => users.id),
analysisType: analysisType("analysis_type").notNull(),
audioFilePath: text("audio_file_path").notNull(),
status: analysisJobStatus("status").notNull(),
errorMessage: text("error_message"),
completedAt: timestamp("completed_at", { withTimezone: true, mode: "date" }),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
userIdIdx: index("analysis_jobs_user_id_idx").on(table.userId),
statusIdx: index("analysis_jobs_status_idx").on(table.status),
createdAtIdx: index("analysis_jobs_created_at_idx").on(table.createdAt),
}));
export const analysisResults = pgTable("analysis_results", {
id: uuid("id").defaultRandom().primaryKey(),
analysisJobId: uuid("analysis_job_id").notNull().unique().references(() => analysisJobs.id),
syntheticScore: doublePrecision("synthetic_score").notNull(),
verdict: detectionVerdict("verdict").notNull(),
confidence: doublePrecision("confidence").notNull(),
processingTimeMs: integer("processing_time_ms").notNull(),
matchedEnrollmentId: text("matched_enrollment_id"),
matchedSimilarity: doublePrecision("matched_similarity"),
modelVersion: text("model_version"),
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).defaultNow().notNull(),
}, (table) => ({
analysisJobIdIdx: index("analysis_results_analysis_job_id_idx").on(table.analysisJobId),
syntheticScoreIdx: index("analysis_results_synthetic_score_idx").on(table.syntheticScore),
verdictIdx: index("analysis_results_verdict_idx").on(table.verdict),
}));