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:
567
pnpm-lock.yaml
generated
567
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
|
||||
442
web/src/server/db/schema.test.ts
Normal file
442
web/src/server/db/schema.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
26
web/src/server/db/schema/alerts.ts
Normal file
26
web/src/server/db/schema/alerts.ts
Normal 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),
|
||||
}));
|
||||
31
web/src/server/db/schema/audit.ts
Normal file
31
web/src/server/db/schema/audit.ts
Normal 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),
|
||||
}));
|
||||
66
web/src/server/db/schema/auth.ts
Normal file
66
web/src/server/db/schema/auth.ts
Normal 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),
|
||||
}));
|
||||
45
web/src/server/db/schema/correlation.ts
Normal file
45
web/src/server/db/schema/correlation.ts
Normal 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),
|
||||
}));
|
||||
41
web/src/server/db/schema/darkwatch.ts
Normal file
41
web/src/server/db/schema/darkwatch.ts
Normal 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),
|
||||
}));
|
||||
31
web/src/server/db/schema/enums.ts
Normal file
31
web/src/server/db/schema/enums.ts
Normal 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"]);
|
||||
59
web/src/server/db/schema/hometitle.ts
Normal file
59
web/src/server/db/schema/hometitle.ts
Normal 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),
|
||||
}));
|
||||
14
web/src/server/db/schema/index.ts
Normal file
14
web/src/server/db/schema/index.ts
Normal 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";
|
||||
43
web/src/server/db/schema/marketing.ts
Normal file
43
web/src/server/db/schema/marketing.ts
Normal 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),
|
||||
}));
|
||||
156
web/src/server/db/schema/relations.ts
Normal file
156
web/src/server/db/schema/relations.ts
Normal 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] }),
|
||||
}));
|
||||
67
web/src/server/db/schema/removebrokers.ts
Normal file
67
web/src/server/db/schema/removebrokers.ts
Normal 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),
|
||||
}));
|
||||
30
web/src/server/db/schema/reports.ts
Normal file
30
web/src/server/db/schema/reports.ts
Normal 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),
|
||||
}));
|
||||
37
web/src/server/db/schema/spamshield.ts
Normal file
37
web/src/server/db/schema/spamshield.ts
Normal 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),
|
||||
}));
|
||||
47
web/src/server/db/schema/subscription.ts
Normal file
47
web/src/server/db/schema/subscription.ts
Normal 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),
|
||||
}));
|
||||
65
web/src/server/db/schema/voiceprint.ts
Normal file
65
web/src/server/db/schema/voiceprint.ts
Normal 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),
|
||||
}));
|
||||
Reference in New Issue
Block a user