diff --git a/drizzle/0002_burly_nightcrawler.sql b/drizzle/0002_burly_nightcrawler.sql new file mode 100644 index 0000000..c1f865f --- /dev/null +++ b/drizzle/0002_burly_nightcrawler.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS "research_briefs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" uuid NOT NULL, + "created_by_user_id" uuid NOT NULL, + "organization_id" text NOT NULL, + "organization_name" text, + "title" text NOT NULL, + "summary" text NOT NULL, + "sources" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "saved_segments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" uuid NOT NULL, + "created_by_user_id" uuid NOT NULL, + "name" text NOT NULL, + "query" text NOT NULL, + "result_limit" integer DEFAULT 20 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..32db10a --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,1330 @@ +{ + "id": "b119553f-44af-49e7-ac65-3a0159140bb7", + "prevId": "e00a7431-1fee-48f0-b260-c65f9f11917d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_requests": { + "name": "ai_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_manifest_hash": { + "name": "context_manifest_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_version": { + "name": "template_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_usd_minor_units": { + "name": "cost_usd_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "result_status": { + "name": "result_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "correlation_id": { + "name": "correlation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consent_records": { + "name": "consent_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "assumptions": { + "name": "assumptions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "selected_option": { + "name": "selected_option", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_review": { + "name": "outcome_review", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_due_at": { + "name": "review_due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "document_classification", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'c2'" + }, + "paperless_id": { + "name": "paperless_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ocr_status": { + "name": "ocr_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "horizon": { + "name": "horizon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "goal_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "membership_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_tenant_user_uq": { + "name": "memberships_tenant_user_uq", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_user_idx": { + "name": "memberships_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.observations": { + "name": "observations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "confidence": { + "name": "confidence", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.opportunities": { + "name": "opportunities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_range_min": { + "name": "value_range_min", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": false + }, + "value_range_max": { + "name": "value_range_max", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": false + }, + "probability": { + "name": "probability", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "legal_name": { + "name": "legal_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registry_id": { + "name": "registry_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_ids": { + "name": "external_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_version": { + "name": "event_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "subject_id": { + "name": "subject_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_briefs": { + "name": "research_briefs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.saved_segments": { + "name": "saved_segments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_limit": { + "name": "result_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "due_at": { + "name": "due_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_entity_type": { + "name": "source_entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_entity_id": { + "name": "source_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transactions": { + "name": "transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "numeric(18, 0)", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_date": { + "name": "transaction_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "evidence_status": { + "name": "evidence_status", + "type": "transaction_evidence_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'missing'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.document_classification": { + "name": "document_classification", + "schema": "public", + "values": [ + "c0", + "c1", + "c2", + "c3", + "c4" + ] + }, + "public.goal_status": { + "name": "goal_status", + "schema": "public", + "values": [ + "not_started", + "on_track", + "at_risk", + "achieved", + "abandoned" + ] + }, + "public.membership_role": { + "name": "membership_role", + "schema": "public", + "values": [ + "owner", + "admin", + "member" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "open", + "in_progress", + "blocked", + "done", + "cancelled" + ] + }, + "public.transaction_evidence_status": { + "name": "transaction_evidence_status", + "schema": "public", + "values": [ + "missing", + "partial", + "complete", + "not_required" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 99dfc9b..f90e019 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1785264962186, "tag": "0001_lovely_brood", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1785268609839, + "tag": "0002_burly_nightcrawler", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 937b708..90c2294 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,10 @@ import { AuditModule } from './audit/audit.module'; import { OrganizationsModule } from './organizations/organizations.module'; import { TasksModule } from './tasks/tasks.module'; import { TenantsModule } from './tenants/tenants.module'; +import { IntelligenceModule } from './intelligence/intelligence.module'; +import { SegmentsModule } from './segments/segments.module'; +import { ResearchBriefsModule } from './research-briefs/research-briefs.module'; +import { BriefingModule } from './briefing/briefing.module'; import { SessionGuard } from './auth/session.guard'; import { TenantGuard } from './auth/tenant.guard'; @@ -42,6 +46,10 @@ function parseRedisConnection(redisUrl: string | undefined) { TenantsModule, OrganizationsModule, TasksModule, + IntelligenceModule, + SegmentsModule, + ResearchBriefsModule, + BriefingModule, ], controllers: [HealthController], providers: [ diff --git a/src/briefing/briefing.controller.ts b/src/briefing/briefing.controller.ts new file mode 100644 index 0000000..9d861cf --- /dev/null +++ b/src/briefing/briefing.controller.ts @@ -0,0 +1,14 @@ +import { Controller, Get } from '@nestjs/common'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { BriefingService } from './briefing.service'; + +@Controller('briefing') +export class BriefingController { + constructor(private readonly briefingService: BriefingService) {} + + @Get('today') + today(@CurrentSession() session: AuthenticatedSession) { + return this.briefingService.today(session); + } +} diff --git a/src/briefing/briefing.module.ts b/src/briefing/briefing.module.ts new file mode 100644 index 0000000..14c21b8 --- /dev/null +++ b/src/briefing/briefing.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { BriefingController } from './briefing.controller'; +import { BriefingService } from './briefing.service'; + +@Module({ + controllers: [BriefingController], + providers: [BriefingService], +}) +export class BriefingModule {} diff --git a/src/briefing/briefing.service.ts b/src/briefing/briefing.service.ts new file mode 100644 index 0000000..fb69330 --- /dev/null +++ b/src/briefing/briefing.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@nestjs/common'; +import { and, asc, eq, gte, isNull, lt, ne } from 'drizzle-orm'; +import { db } from '../db/client'; +import { organizations, researchBriefs, savedSegments, tasks } from '../db/schema'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; + +const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; + +/** + * Blueprint 17 "Daily Briefing": Scheduler -> collect tasks/deadlines -> deterministic + * priority -> AI explanation -> save -> notify. Aici implementam doar partea + * deterministica (colectare + prioritizare); explicatia AI vine cu AI Gateway-ul + * (blueprint 14), inca neconstruit -- nu simulam text generat de AI. + */ +@Injectable() +export class BriefingService { + async today(session: AuthenticatedSession) { + const now = new Date(); + const weekAgo = new Date(now.getTime() - SEVEN_DAYS_MS); + const weekFromNow = new Date(now.getTime() + SEVEN_DAYS_MS); + + const activeStatuses = [ne(tasks.status, 'done'), ne(tasks.status, 'cancelled')] as const; + + const [overdueTasks, upcomingTasks, newOrganizations, recentSegments, recentBriefs] = + await Promise.all([ + db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, session.tenantId), + isNull(tasks.deletedAt), + ...activeStatuses, + lt(tasks.dueAt, now), + ), + orderBy: asc(tasks.dueAt), + }), + db.query.tasks.findMany({ + where: and( + eq(tasks.tenantId, session.tenantId), + isNull(tasks.deletedAt), + ...activeStatuses, + gte(tasks.dueAt, now), + lt(tasks.dueAt, weekFromNow), + ), + orderBy: [asc(tasks.priority), asc(tasks.dueAt)], + }), + db.query.organizations.findMany({ + where: and( + eq(organizations.tenantId, session.tenantId), + isNull(organizations.deletedAt), + gte(organizations.createdAt, weekAgo), + ), + }), + db.query.savedSegments.findMany({ + where: and(eq(savedSegments.tenantId, session.tenantId), gte(savedSegments.createdAt, weekAgo)), + }), + db.query.researchBriefs.findMany({ + where: and(eq(researchBriefs.tenantId, session.tenantId), gte(researchBriefs.createdAt, weekAgo)), + }), + ]); + + return { + generatedAt: now.toISOString(), + overdueTasks, + upcomingTasks, + weekInReview: { + newOrganizations: newOrganizations.length, + newSegments: recentSegments.length, + newResearchBriefs: recentBriefs.length, + }, + // camp explicit -- semnaleaza in UI ca lipseste inca stratul AI, nu-l ascunde + aiExplanation: null, + }; + } +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 19c775d..c5f3932 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -245,3 +245,33 @@ export const auditLog = pgTable('audit_log', { correlationId: uuid('correlation_id'), createdAt: timestamp('created_at').defaultNow().notNull(), }); + +// --- Intelligence Engine / Faza A2 (blueprint sectiunea 10.3, 17 "Lead Intelligence") --- +// Apollo/ClickHouse raman sursa de adevar pentru datele B2B (blueprint 9: "Private data != +// B2B intelligence"); aici pastram doar referinte (organization_id extern) si curatoria +// tenantului -- cautari salvate si dovezi de research, nu o copie a datasetului. + +export const savedSegments = pgTable('saved_segments', { + id: uuid('id').defaultRandom().primaryKey(), + tenantId: uuid('tenant_id').notNull(), + createdByUserId: uuid('created_by_user_id').notNull(), + name: text('name').notNull(), + query: text('query').notNull(), + resultLimit: integer('result_limit').notNull().default(20), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +// summary e curatat manual de user in absenta AI Gateway-ului (blueprint sectiunea 14); +// schema e gandita ca AI Gateway sa poata popula acelasi camp mai tarziu fara migrare +export const researchBriefs = pgTable('research_briefs', { + id: uuid('id').defaultRandom().primaryKey(), + tenantId: uuid('tenant_id').notNull(), + createdByUserId: uuid('created_by_user_id').notNull(), + organizationId: text('organization_id').notNull(), + organizationName: text('organization_name'), + title: text('title').notNull(), + summary: text('summary').notNull(), + sources: jsonb('sources').notNull().default([]), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); diff --git a/src/intelligence/dto.ts b/src/intelligence/dto.ts new file mode 100644 index 0000000..210a83e --- /dev/null +++ b/src/intelligence/dto.ts @@ -0,0 +1,14 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsString, Length, Max, Min } from 'class-validator'; + +export class SearchCompaniesQueryDto { + @IsString() + @Length(2, 200) + q!: string; + + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit: number = 20; +} diff --git a/src/intelligence/intelligence.controller.ts b/src/intelligence/intelligence.controller.ts new file mode 100644 index 0000000..99b0749 --- /dev/null +++ b/src/intelligence/intelligence.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { SearchCompaniesQueryDto } from './dto'; +import { IntelligenceService } from './intelligence.service'; + +@Controller('intelligence/companies') +export class IntelligenceController { + constructor(private readonly intelligenceService: IntelligenceService) {} + + @Get('search') + search(@Query() query: SearchCompaniesQueryDto) { + return this.intelligenceService.searchCompanies(query.q, query.limit); + } + + @Get(':organizationId') + getCompany(@Param('organizationId') organizationId: string) { + return this.intelligenceService.getCompany(organizationId); + } +} diff --git a/src/intelligence/intelligence.module.ts b/src/intelligence/intelligence.module.ts new file mode 100644 index 0000000..3b5767a --- /dev/null +++ b/src/intelligence/intelligence.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { IntelligenceController } from './intelligence.controller'; +import { IntelligenceService } from './intelligence.service'; + +@Module({ + controllers: [IntelligenceController], + providers: [IntelligenceService], + exports: [IntelligenceService], +}) +export class IntelligenceModule {} diff --git a/src/intelligence/intelligence.service.ts b/src/intelligence/intelligence.service.ts new file mode 100644 index 0000000..604367c --- /dev/null +++ b/src/intelligence/intelligence.service.ts @@ -0,0 +1,55 @@ +import { BadGatewayException, Injectable, NotFoundException } from '@nestjs/common'; +import type { CompanySearchResponse, CompanySearchResult } from './intelligence.types'; + +const REQUEST_TIMEOUT_MS = 8000; + +/** + * Client subtire catre intelligence-api. Blueprint 10.3/16: "company search prin + * Intelligence API, nu SQL direct" -- ceo-web nu vorbeste niciodata direct cu + * intelligence-api, ca sa putem adauga aici audit, quotas si viitorul AI Gateway + * fara sa schimbam contractul catre frontend. + */ +@Injectable() +export class IntelligenceService { + private readonly baseUrl = process.env.INTELLIGENCE_API_URL ?? 'http://localhost:8000'; + + async searchCompanies(query: string, limit: number): Promise { + const response = await this.request('/companies/search', { + method: 'POST', + body: JSON.stringify({ query, limit }), + }); + return (await response.json()) as CompanySearchResponse; + } + + async getCompany(organizationId: string): Promise { + const response = await this.request(`/companies/${encodeURIComponent(organizationId)}`); + const data = (await response.json()) as CompanySearchResult | { error: string }; + if ('error' in data) { + throw new NotFoundException('Company not found in intelligence data'); + } + return data; + } + + private async request(path: string, init: RequestInit = {}): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const response = await fetch(`${this.baseUrl}${path}`, { + ...init, + headers: { 'Content-Type': 'application/json', ...init.headers }, + signal: controller.signal, + }); + if (!response.ok) { + throw new BadGatewayException('intelligence-api request failed'); + } + return response; + } catch (error) { + if (error instanceof BadGatewayException) { + throw error; + } + throw new BadGatewayException('intelligence-api unreachable'); + } finally { + clearTimeout(timeout); + } + } +} diff --git a/src/intelligence/intelligence.types.ts b/src/intelligence/intelligence.types.ts new file mode 100644 index 0000000..21cc83c --- /dev/null +++ b/src/intelligence/intelligence.types.ts @@ -0,0 +1,17 @@ +// Forma exacta intoarsa de intelligence-api (app/routers/companies.py SEARCH_COLUMNS) -- +// tinuta sincronizata manual, nu e generata; intelligence-api nu publica un schema versionat. +export interface CompanySearchResult { + organization_id: string; + organization_name: string; + normalized_domain: string; + hq_city: string; + hq_country: string; + industries: string[]; + num_current_employees: number | null; + revenue_in_thousands: number | null; +} + +export interface CompanySearchResponse { + results: CompanySearchResult[]; + count: number; +} diff --git a/src/research-briefs/dto.ts b/src/research-briefs/dto.ts new file mode 100644 index 0000000..3f4697d --- /dev/null +++ b/src/research-briefs/dto.ts @@ -0,0 +1,36 @@ +import { Type } from 'class-transformer'; +import { IsArray, IsOptional, IsString, IsUrl, Length, ValidateNested } from 'class-validator'; + +export class SourceDto { + @IsString() + @Length(1, 200) + label!: string; + + @IsUrl() + url!: string; +} + +export class CreateResearchBriefDto { + @IsString() + @Length(1, 128) + organizationId!: string; + + @IsOptional() + @IsString() + @Length(1, 250) + organizationName?: string; + + @IsString() + @Length(1, 250) + title!: string; + + @IsString() + @Length(1, 5000) + summary!: string; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SourceDto) + sources?: SourceDto[]; +} diff --git a/src/research-briefs/research-briefs.controller.ts b/src/research-briefs/research-briefs.controller.ts new file mode 100644 index 0000000..b6e60be --- /dev/null +++ b/src/research-briefs/research-briefs.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { CreateResearchBriefDto } from './dto'; +import { ResearchBriefsService } from './research-briefs.service'; + +@Controller('research-briefs') +export class ResearchBriefsController { + constructor(private readonly researchBriefsService: ResearchBriefsService) {} + + @Get() + list(@CurrentSession() session: AuthenticatedSession) { + return this.researchBriefsService.list(session); + } + + @Get(':id') + getById(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + return this.researchBriefsService.getById(session, id); + } + + @Post() + create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateResearchBriefDto) { + return this.researchBriefsService.create(session, dto); + } + + @Delete(':id') + remove(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + return this.researchBriefsService.remove(session, id); + } +} diff --git a/src/research-briefs/research-briefs.module.ts b/src/research-briefs/research-briefs.module.ts new file mode 100644 index 0000000..3815a5d --- /dev/null +++ b/src/research-briefs/research-briefs.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ResearchBriefsController } from './research-briefs.controller'; +import { ResearchBriefsService } from './research-briefs.service'; + +@Module({ + controllers: [ResearchBriefsController], + providers: [ResearchBriefsService], +}) +export class ResearchBriefsModule {} diff --git a/src/research-briefs/research-briefs.service.ts b/src/research-briefs/research-briefs.service.ts new file mode 100644 index 0000000..2b37815 --- /dev/null +++ b/src/research-briefs/research-briefs.service.ts @@ -0,0 +1,71 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { and, desc, eq } from 'drizzle-orm'; +import { db } from '../db/client'; +import { researchBriefs } from '../db/schema'; +import { AuditService } from '../audit/audit.service'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import type { CreateResearchBriefDto } from './dto'; + +@Injectable() +export class ResearchBriefsService { + constructor(private readonly audit: AuditService) {} + + async list(session: AuthenticatedSession) { + return db.query.researchBriefs.findMany({ + where: eq(researchBriefs.tenantId, session.tenantId), + orderBy: desc(researchBriefs.createdAt), + }); + } + + async getById(session: AuthenticatedSession, id: string) { + const brief = await db.query.researchBriefs.findFirst({ + where: and(eq(researchBriefs.id, id), eq(researchBriefs.tenantId, session.tenantId)), + }); + if (!brief) { + throw new NotFoundException('Research brief not found'); + } + return brief; + } + + async create(session: AuthenticatedSession, dto: CreateResearchBriefDto) { + // Provenance obligatoriu (blueprint 9.1 Evidence & Provenance Fabric): chiar in + // versiunea curatata manual, fiecare brief indica cel putin sursa Apollo/intelligence-api. + const sources = [ + { label: 'Apollo / intelligence-api', url: 'https://intelligence-api.boardmind.dev' }, + ...(dto.sources ?? []), + ]; + + const [row] = await db + .insert(researchBriefs) + .values({ + tenantId: session.tenantId, + createdByUserId: session.userId, + organizationId: dto.organizationId, + organizationName: dto.organizationName, + title: dto.title, + summary: dto.summary, + sources, + }) + .returning(); + + await this.audit.record(null, { + tenantId: session.tenantId, + actorId: session.userId, + action: 'research_brief.created', + resource: `research_brief:${row.id}`, + }); + return row; + } + + async remove(session: AuthenticatedSession, id: string) { + const brief = await this.getById(session, id); + await db.delete(researchBriefs).where(eq(researchBriefs.id, brief.id)); + await this.audit.record(null, { + tenantId: session.tenantId, + actorId: session.userId, + action: 'research_brief.deleted', + resource: `research_brief:${id}`, + }); + return { deleted: true }; + } +} diff --git a/src/segments/dto.ts b/src/segments/dto.ts new file mode 100644 index 0000000..4297fa4 --- /dev/null +++ b/src/segments/dto.ts @@ -0,0 +1,16 @@ +import { IsInt, IsString, Length, Max, Min } from 'class-validator'; + +export class CreateSegmentDto { + @IsString() + @Length(1, 200) + name!: string; + + @IsString() + @Length(2, 200) + query!: string; + + @IsInt() + @Min(1) + @Max(100) + resultLimit: number = 20; +} diff --git a/src/segments/segments.controller.ts b/src/segments/segments.controller.ts new file mode 100644 index 0000000..4754745 --- /dev/null +++ b/src/segments/segments.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { CreateSegmentDto } from './dto'; +import { SegmentsService } from './segments.service'; + +@Controller('segments') +export class SegmentsController { + constructor(private readonly segmentsService: SegmentsService) {} + + @Get() + list(@CurrentSession() session: AuthenticatedSession) { + return this.segmentsService.list(session); + } + + @Post() + create(@CurrentSession() session: AuthenticatedSession, @Body() dto: CreateSegmentDto) { + return this.segmentsService.create(session, dto); + } + + @Get(':id/run') + run(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + return this.segmentsService.run(session, id); + } + + @Delete(':id') + remove(@CurrentSession() session: AuthenticatedSession, @Param('id', ParseUUIDPipe) id: string) { + return this.segmentsService.remove(session, id); + } +} diff --git a/src/segments/segments.module.ts b/src/segments/segments.module.ts new file mode 100644 index 0000000..00a7965 --- /dev/null +++ b/src/segments/segments.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { IntelligenceModule } from '../intelligence/intelligence.module'; +import { SegmentsController } from './segments.controller'; +import { SegmentsService } from './segments.service'; + +@Module({ + imports: [IntelligenceModule], + controllers: [SegmentsController], + providers: [SegmentsService], +}) +export class SegmentsModule {} diff --git a/src/segments/segments.service.ts b/src/segments/segments.service.ts new file mode 100644 index 0000000..13efb30 --- /dev/null +++ b/src/segments/segments.service.ts @@ -0,0 +1,72 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { and, desc, eq } from 'drizzle-orm'; +import { db } from '../db/client'; +import { savedSegments } from '../db/schema'; +import { AuditService } from '../audit/audit.service'; +import { IntelligenceService } from '../intelligence/intelligence.service'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import type { CreateSegmentDto } from './dto'; + +@Injectable() +export class SegmentsService { + constructor( + private readonly audit: AuditService, + private readonly intelligence: IntelligenceService, + ) {} + + async list(session: AuthenticatedSession) { + return db.query.savedSegments.findMany({ + where: eq(savedSegments.tenantId, session.tenantId), + orderBy: desc(savedSegments.createdAt), + }); + } + + async create(session: AuthenticatedSession, dto: CreateSegmentDto) { + const [row] = await db + .insert(savedSegments) + .values({ + tenantId: session.tenantId, + createdByUserId: session.userId, + name: dto.name, + query: dto.query, + resultLimit: dto.resultLimit, + }) + .returning(); + await this.audit.record(null, { + tenantId: session.tenantId, + actorId: session.userId, + action: 'segment.created', + resource: `segment:${row.id}`, + }); + return row; + } + + /** Re-ruleaza cautarea salvata contra intelligence-api si intoarce rezultate proaspete. */ + async run(session: AuthenticatedSession, id: string) { + const segment = await this.getById(session, id); + const results = await this.intelligence.searchCompanies(segment.query, segment.resultLimit); + return { segment, ...results }; + } + + async remove(session: AuthenticatedSession, id: string) { + const segment = await this.getById(session, id); + await db.delete(savedSegments).where(eq(savedSegments.id, segment.id)); + await this.audit.record(null, { + tenantId: session.tenantId, + actorId: session.userId, + action: 'segment.deleted', + resource: `segment:${id}`, + }); + return { deleted: true }; + } + + private async getById(session: AuthenticatedSession, id: string) { + const segment = await db.query.savedSegments.findFirst({ + where: and(eq(savedSegments.id, id), eq(savedSegments.tenantId, session.tenantId)), + }); + if (!segment) { + throw new NotFoundException('Segment not found'); + } + return segment; + } +}