From ef1ce0872f00cac7069270f84962dadbc33eb9cc Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:51:41 +0200 Subject: [PATCH 1/5] refactor(db): Drizzle schema with proven byte-parity to the Prisma-shaped live DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drizzle-kit pull against a scratch Postgres built by the 29 Prisma migrations, then: mode 'date', $defaultFn(createId) on the 43 cuid ids, $defaultFn/$onUpdateFn on the 29 @updatedAt tables, bytea customType for the two Bytes columns, ARRAY[]::TEXT[] defaults restored (drizzle-kit misparses them as ["RAY"]), and per-column .op() specifiers stripped (drizzle-kit scrambles them positionally in multi-column indexes; they were all Postgres defaults anyway). Parity proof: Prisma chain -> db A, drizzle 0000 -> db B, normalized pg_dump --schema-only diff = EMPTY (1486 DDL lines each; tables, enums, indexes, FKs, uniques, the Message_one_author CHECK — names included). Relations mirror Prisma's field names so include:->with: reads identically; types.ts reproduces Prisma's per-enum runtime objects and row types. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn --- drizzle.config.ts | 17 + drizzle/0000_unusual_steel_serpent.sql | 970 +++ drizzle/meta/0000_snapshot.json | 8152 ++++++++++++++++++ drizzle/meta/_journal.json | 13 + package-lock.json | 10123 +++++++++++++---------- package.json | 5 + src/lib/db.ts | 15 - src/lib/db/index.ts | 32 + src/lib/db/relations.ts | 697 ++ src/lib/db/schema.ts | 1515 ++++ src/lib/db/types.ts | 683 ++ 11 files changed, 18032 insertions(+), 4190 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_unusual_steel_serpent.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json delete mode 100644 src/lib/db.ts create mode 100644 src/lib/db/index.ts create mode 100644 src/lib/db/relations.ts create mode 100644 src/lib/db/schema.ts create mode 100644 src/lib/db/types.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..c79b8193 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'drizzle-kit' + +// Fleet house pattern (see reparaturbonus-zh, vitareba). Migrations in +// ./drizzle are applied two ways: +// - fresh databases (dev, CI service containers): `npm run db:migrate` +// (drizzle-kit's own journal) +// - the live box: fleetcrown's scripts/hetzner/apply-schema.sh on every +// deploy — forward-only, ledgered in public._deploy_schema_history, +// refuses destructive statements +export default defineConfig({ + schema: ['./src/lib/db/schema.ts', './src/lib/db/relations.ts'], + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}) diff --git a/drizzle/0000_unusual_steel_serpent.sql b/drizzle/0000_unusual_steel_serpent.sql new file mode 100644 index 00000000..f3808fa2 --- /dev/null +++ b/drizzle/0000_unusual_steel_serpent.sql @@ -0,0 +1,970 @@ +CREATE TYPE "public"."ActivityCategory" AS ENUM('SPORT', 'LANGUAGE', 'CULTURE', 'COMMUNITY', 'FAMILY', 'SUPPORT');--> statement-breakpoint +CREATE TYPE "public"."ActivityCost" AS ENUM('FREE', 'REDUCED', 'PAID');--> statement-breakpoint +CREATE TYPE "public"."ActivityStatus" AS ENUM('DRAFT', 'PUBLISHED', 'ARCHIVED');--> statement-breakpoint +CREATE TYPE "public"."AgeRange" AS ENUM('YOUNG_ADULT', 'ADULT', 'MIDDLE_AGED', 'SENIOR');--> statement-breakpoint +CREATE TYPE "public"."AgreementStatus" AS ENUM('PROPOSED', 'ACCEPTED', 'HELD', 'BROKEN', 'EXPIRED');--> statement-breakpoint +CREATE TYPE "public"."ApplicationStage" AS ENUM('INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED', 'STARTED', 'ENDED', 'DECLINED');--> statement-breakpoint +CREATE TYPE "public"."AppointmentStatus" AS ENUM('SCHEDULED', 'COMPLETED', 'CANCELLED', 'NO_SHOW', 'REQUESTED');--> statement-breakpoint +CREATE TYPE "public"."AuthTokenPurpose" AS ENUM('VERIFY_EMAIL', 'RESET_PASSWORD');--> statement-breakpoint +CREATE TYPE "public"."CareRole" AS ENUM('HOUSING', 'SOCIAL', 'JOB', 'VOLUNTEERING');--> statement-breakpoint +CREATE TYPE "public"."CheckInType" AS ENUM('INITIAL', 'REGULAR', 'AD_HOC', 'EXIT');--> statement-breakpoint +CREATE TYPE "public"."ComplaintStatus" AS ENUM('OPEN', 'IN_REVIEW', 'ANSWERED');--> statement-breakpoint +CREATE TYPE "public"."ComplaintSubject" AS ENUM('STAFF', 'ACCOMMODATION', 'DECISION', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."ConflictStyle" AS ENUM('AVOIDANT', 'COOPERATIVE', 'DIRECT');--> statement-breakpoint +CREATE TYPE "public"."DecisionMode" AS ENUM('RESIDENT_BINDING', 'RESIDENT_ADVISORY', 'STAFF_ONLY');--> statement-breakpoint +CREATE TYPE "public"."EndReason" AS ENUM('NATURAL', 'CONFLICT', 'REQUEST', 'CAPACITY', 'UPGRADE', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."EventRsvpStatus" AS ENUM('GOING', 'MAYBE', 'DECLINED');--> statement-breakpoint +CREATE TYPE "public"."FamilyStatus" AS ENUM('SINGLE', 'COUPLE', 'FAMILY_WITH_CHILDREN', 'SINGLE_PARENT');--> statement-breakpoint +CREATE TYPE "public"."FollowUpPriority" AS ENUM('LOW', 'NORMAL', 'HIGH', 'URGENT');--> statement-breakpoint +CREATE TYPE "public"."Gender" AS ENUM('MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_SAY');--> statement-breakpoint +CREATE TYPE "public"."HouseEventCategory" AS ENUM('HOUSE_MEETING', 'SOCIAL', 'CULTURE', 'SUPPORT');--> statement-breakpoint +CREATE TYPE "public"."HouseEventStatus" AS ENUM('DRAFT', 'PUBLISHED', 'CANCELLED');--> statement-breakpoint +CREATE TYPE "public"."HouseholdTaskCategory" AS ENUM('CLEANING', 'SHOPPING', 'MAINTENANCE', 'COOKING', 'TRASH', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."HouseholdTaskPriority" AS ENUM('LOW', 'NORMAL', 'HIGH', 'URGENT');--> statement-breakpoint +CREATE TYPE "public"."HouseholdTaskStatus" AS ENUM('IDLE', 'NEEDS_ATTENTION', 'REQUESTED', 'IN_PROGRESS');--> statement-breakpoint +CREATE TYPE "public"."HouseholdTaskType" AS ENUM('ONE_TIME', 'RECURRING_SCHEDULED', 'RECURRING_AS_NEEDED');--> statement-breakpoint +CREATE TYPE "public"."HousingStatus" AS ENUM('AVAILABLE', 'FULL', 'MAINTENANCE', 'CLOSED');--> statement-breakpoint +CREATE TYPE "public"."IncidentCategory" AS ENUM('INTERPERSONAL', 'MAINTENANCE', 'SAFETY', 'WELLBEING');--> statement-breakpoint +CREATE TYPE "public"."IncidentSeverity" AS ENUM('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');--> statement-breakpoint +CREATE TYPE "public"."IncidentType" AS ENUM('NOISE_COMPLAINT', 'CLEANLINESS_DISPUTE', 'PERSONAL_CONFLICT', 'CULTURAL_FRICTION', 'SPACE_DISPUTE', 'SCHEDULE_CONFLICT', 'SAFETY_CONCERN', 'PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY_SYSTEM', 'GENERAL_MAINTENANCE', 'LOW_SATISFACTION', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."InvolvementRole" AS ENUM('INVOLVED', 'WITNESS', 'MEDIATOR');--> statement-breakpoint +CREATE TYPE "public"."LearningKind" AS ENUM('LANGUAGE_TEST', 'COURSE', 'INFORMAL', 'QUALIFICATION', 'VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP');--> statement-breakpoint +CREATE TYPE "public"."LearningStatus" AS ENUM('PLANNED', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED');--> statement-breakpoint +CREATE TYPE "public"."LivingSkillsSupport" AS ENUM('INDEPENDENT', 'SOME_SUPPORT', 'REGULAR_SUPPORT');--> statement-breakpoint +CREATE TYPE "public"."MaintenanceCategory" AS ENUM('PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY', 'CLEANING', 'EXTERIOR', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."MaintenancePriority" AS ENUM('LOW', 'NORMAL', 'HIGH', 'URGENT');--> statement-breakpoint +CREATE TYPE "public"."MaintenanceStatus" AS ENUM('OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD', 'COMPLETED', 'CANCELLED');--> statement-breakpoint +CREATE TYPE "public"."MarketplacePostKind" AS ENUM('GIVE_AWAY', 'LEND', 'WANTED', 'OFFER_HELP', 'NEED_HELP');--> statement-breakpoint +CREATE TYPE "public"."MarketplacePostStatus" AS ENUM('OPEN', 'CLAIMED', 'CLOSED');--> statement-breakpoint +CREATE TYPE "public"."MedicalDocType" AS ENUM('PRIVATE_ROOM', 'STUDIO', 'BOTH');--> statement-breakpoint +CREATE TYPE "public"."MobilityNeed" AS ENUM('NONE', 'GROUND_FLOOR', 'WHEELCHAIR');--> statement-breakpoint +CREATE TYPE "public"."OpportunityKind" AS ENUM('VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP');--> statement-breakpoint +CREATE TYPE "public"."OpportunityStatus" AS ENUM('DRAFT', 'PUBLISHED', 'ARCHIVED');--> statement-breakpoint +CREATE TYPE "public"."PermitRequirement" AS ENUM('NONE', 'EMPLOYER_NOTIFIES', 'PERMIT_REQUIRED');--> statement-breakpoint +CREATE TYPE "public"."PlacementStatus" AS ENUM('ACTIVE', 'ENDED', 'TRANSFERRED');--> statement-breakpoint +CREATE TYPE "public"."ProfileVisibility" AS ENUM('PRIVATE', 'ROOMMATES', 'RESIDENTS');--> statement-breakpoint +CREATE TYPE "public"."ProposalStatus" AS ENUM('DISCUSSION', 'VOTING', 'NEEDS_STAFF_CONFIRMATION', 'ACCEPTED', 'REJECTED', 'WITHDRAWN', 'VETOED', 'EXPIRED');--> statement-breakpoint +CREATE TYPE "public"."ProposalType" AS ENUM('ADD_RULE', 'AMEND_RULE', 'REPEAL_RULE', 'HOUSE_DECISION');--> statement-breakpoint +CREATE TYPE "public"."RecyclingKnowledge" AS ENUM('NONE', 'BASIC', 'GOOD');--> statement-breakpoint +CREATE TYPE "public"."ResidentOrStaff" AS ENUM('RESIDENT', 'STAFF');--> statement-breakpoint +CREATE TYPE "public"."ResidentStatus" AS ENUM('ACTIVE', 'PLACED', 'TRANSFERRED', 'EXITED');--> statement-breakpoint +CREATE TYPE "public"."ResolutionStage" AS ENUM('REPORTED', 'SELF_RESOLUTION', 'PEER_MEDIATION', 'STAFF_MEDIATION', 'FORMAL_MEASURE', 'CLOSED');--> statement-breakpoint +CREATE TYPE "public"."RoomSharingStatus" AS ENUM('CAN_SHARE', 'PREFERS_PRIVATE', 'NEEDS_PRIVATE');--> statement-breakpoint +CREATE TYPE "public"."RuleCategory" AS ENUM('SAFETY', 'RESPECT', 'NOISE', 'CLEANLINESS', 'KITCHEN', 'BATHROOM', 'GUESTS', 'SHARED_SPACES', 'COSTS', 'COMMUNICATION', 'OTHER');--> statement-breakpoint +CREATE TYPE "public"."RuleDelegation" AS ENUM('FIXED', 'UNIT_MAY_STRENGTHEN', 'UNIT_DECIDES');--> statement-breakpoint +CREATE TYPE "public"."RuleScope" AS ENUM('ORG', 'UNIT');--> statement-breakpoint +CREATE TYPE "public"."RuleStatus" AS ENUM('ACTIVE', 'SUPERSEDED', 'ARCHIVED');--> statement-breakpoint +CREATE TYPE "public"."SleepSchedule" AS ENUM('EARLY_BIRD', 'STANDARD', 'NIGHT_OWL', 'IRREGULAR');--> statement-breakpoint +CREATE TYPE "public"."SmokingStatus" AS ENUM('NON_SMOKER', 'OUTDOOR_SMOKER', 'INDOOR_SMOKER');--> statement-breakpoint +CREATE TYPE "public"."SocialStyle" AS ENUM('INTROVERTED', 'MODERATE', 'EXTROVERTED');--> statement-breakpoint +CREATE TYPE "public"."SpotStatus" AS ENUM('AVAILABLE', 'OCCUPIED', 'MAINTENANCE', 'CLOSED');--> statement-breakpoint +CREATE TYPE "public"."SpotType" AS ENUM('BED', 'PRIVATE_ROOM', 'STUDIO', 'ROOM');--> statement-breakpoint +CREATE TYPE "public"."StaffDecision" AS ENUM('CONFIRMED', 'VETOED');--> statement-breakpoint +CREATE TYPE "public"."StaffRole" AS ENUM('ADMIN', 'BETREUUNG', 'SOZIALARBEIT', 'JOBCOACH', 'FREIWILLIGENARBEIT');--> statement-breakpoint +CREATE TYPE "public"."StaffScope" AS ENUM('OWN_DOMAIN', 'ALL_DOMAINS');--> statement-breakpoint +CREATE TYPE "public"."SupportLevel" AS ENUM('STANDARD', 'ELEVATED', 'INTENSIVE');--> statement-breakpoint +CREATE TYPE "public"."TaskRequestStatus" AS ENUM('PENDING', 'ACCEPTED', 'DECLINED', 'COMPLETED');--> statement-breakpoint +CREATE TYPE "public"."TransferRequestStatus" AS ENUM('PENDING', 'APPROVED', 'DENIED', 'COMPLETED', 'CANCELLED');--> statement-breakpoint +CREATE TYPE "public"."VoteChoice" AS ENUM('YES', 'NO', 'ABSTAIN', 'BLOCK');--> statement-breakpoint +CREATE TYPE "public"."VoteThreshold" AS ENUM('CONSENSUS', 'SUPERMAJORITY', 'SIMPLE_MAJORITY');--> statement-breakpoint +CREATE TABLE "Account" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "email" text NOT NULL, + "passwordHash" text, + "emailVerifiedAt" timestamp (3), + "userId" text, + "residentId" text +); +--> statement-breakpoint +CREATE TABLE "Activity" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "category" "ActivityCategory" NOT NULL, + "cost" "ActivityCost" DEFAULT 'FREE' NOT NULL, + "costNote" text, + "location" text, + "website" text, + "phone" text, + "schedule" text, + "startsAt" timestamp (3), + "endsAt" timestamp (3), + "status" "ActivityStatus" DEFAULT 'DRAFT' NOT NULL, + "highlight" boolean DEFAULT false NOT NULL, + "createdByUserId" text, + "updatedByUserId" text +); +--> statement-breakpoint +CREATE TABLE "AgreementParty" ( + "id" text PRIMARY KEY NOT NULL, + "agreementId" text NOT NULL, + "residentId" text NOT NULL, + "acceptedAt" timestamp (3), + "declinedAt" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "AlgorithmWeight" ( + "id" text PRIMARY KEY NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "lifestyleWeight" double precision DEFAULT 30 NOT NULL, + "socialWeight" double precision DEFAULT 25 NOT NULL, + "practicalWeight" double precision DEFAULT 25 NOT NULL, + "riskWeight" double precision DEFAULT 20 NOT NULL, + "factorWeights" jsonb NOT NULL, + "version" integer DEFAULT 1 NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "notes" text +); +--> statement-breakpoint +CREATE TABLE "Appointment" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "staffId" text, + "domain" "CareRole" NOT NULL, + "title" text NOT NULL, + "startsAt" timestamp (3) NOT NULL, + "endsAt" timestamp (3), + "location" text, + "notes" text, + "status" "AppointmentStatus" DEFAULT 'SCHEDULED' NOT NULL, + "residentNote" text, + "staffNote" text +); +--> statement-breakpoint +CREATE TABLE "AuditLog" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "action" text NOT NULL, + "entity" text NOT NULL, + "entityId" text NOT NULL, + "userId" text, + "changes" jsonb, + "reason" text +); +--> statement-breakpoint +CREATE TABLE "AuthToken" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "tokenHash" text NOT NULL, + "purpose" "AuthTokenPurpose" NOT NULL, + "expiresAt" timestamp (3) NOT NULL, + "usedAt" timestamp (3), + "accountId" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "CareAssignment" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "staffId" text NOT NULL, + "role" "CareRole" NOT NULL +); +--> statement-breakpoint +CREATE TABLE "CareAttribute" ( + "id" text PRIMARY KEY NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "domain" "CareRole" NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "updatedById" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "CompatibilityAssessment" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "residentId" text NOT NULL, + "comparedWithId" text NOT NULL, + "overallScore" double precision NOT NULL, + "lifestyleScore" double precision NOT NULL, + "socialScore" double precision NOT NULL, + "practicalScore" double precision NOT NULL, + "riskScore" double precision NOT NULL, + "strengths" text[], + "concerns" text[], + "recommendations" text[] +); +--> statement-breakpoint +CREATE TABLE "Complaint" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text, + "subject" "ComplaintSubject" NOT NULL, + "body" text NOT NULL, + "status" "ComplaintStatus" DEFAULT 'OPEN' NOT NULL, + "response" text, + "respondedAt" timestamp (3), + "respondedByUserId" text +); +--> statement-breakpoint +CREATE TABLE "ConflictAgreement" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "incidentId" text NOT NULL, + "terms" text NOT NULL, + "mediatorName" text, + "reviewDate" timestamp (3) NOT NULL, + "status" "AgreementStatus" DEFAULT 'PROPOSED' NOT NULL, + "outcomeNotes" text, + "reviewedAt" timestamp (3), + "ruleProposalId" text +); +--> statement-breakpoint +CREATE TABLE "EventRsvp" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "eventId" text NOT NULL, + "residentId" text NOT NULL, + "status" "EventRsvpStatus" DEFAULT 'GOING' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "Expense" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "paidById" text NOT NULL, + "createdById" text NOT NULL, + "description" text NOT NULL, + "category" text NOT NULL, + "amountRappen" integer NOT NULL, + "date" timestamp (3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ExpenseShare" ( + "id" text PRIMARY KEY NOT NULL, + "expenseId" text NOT NULL, + "residentId" text NOT NULL, + "amountRappen" integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE "HouseEvent" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "category" "HouseEventCategory" DEFAULT 'SOCIAL' NOT NULL, + "location" text, + "startsAt" timestamp (3) NOT NULL, + "endsAt" timestamp (3), + "status" "HouseEventStatus" DEFAULT 'PUBLISHED' NOT NULL, + "createdByStaffId" text, + "createdByResidentId" text +); +--> statement-breakpoint +CREATE TABLE "HouseRule" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "scope" "RuleScope" NOT NULL, + "housingUnitId" text, + "key" text, + "category" "RuleCategory" NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "delegation" "RuleDelegation" DEFAULT 'FIXED' NOT NULL, + "parentRuleId" text, + "status" "RuleStatus" DEFAULT 'ACTIVE' NOT NULL, + "version" integer DEFAULT 1 NOT NULL, + "effectiveFrom" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "effectiveUntil" timestamp (3), + "adoptedByProposalId" text, + "createdByStaff" text +); +--> statement-breakpoint +CREATE TABLE "HouseholdTask" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "title" text NOT NULL, + "description" text, + "instructions" text, + "taskType" "HouseholdTaskType" DEFAULT 'ONE_TIME' NOT NULL, + "category" "HouseholdTaskCategory" DEFAULT 'OTHER' NOT NULL, + "priority" "HouseholdTaskPriority" DEFAULT 'NORMAL' NOT NULL, + "scheduleHuman" text, + "estimatedMinutes" integer, + "currentStatus" "HouseholdTaskStatus" DEFAULT 'IDLE' NOT NULL, + "isCompleted" boolean DEFAULT false NOT NULL, + "completedAt" timestamp (3), + "createdByResidentId" text, + "createdByStaff" text, + "checklist" text[] DEFAULT ARRAY[]::TEXT[], + "rotationResidentIds" text[] DEFAULT ARRAY[]::TEXT[] +); +--> statement-breakpoint +CREATE TABLE "HousingUnit" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "code" text NOT NULL, + "address" text NOT NULL, + "totalBeds" integer NOT NULL, + "totalRooms" integer NOT NULL, + "sharedRooms" integer NOT NULL, + "privateRooms" integer NOT NULL, + "sharedBathrooms" integer NOT NULL, + "privateBathrooms" integer NOT NULL, + "sharedKitchen" boolean DEFAULT true NOT NULL, + "privateKitchen" boolean DEFAULT false NOT NULL, + "groundFloor" boolean DEFAULT false NOT NULL, + "wheelchairAccess" boolean DEFAULT false NOT NULL, + "elevator" boolean DEFAULT false NOT NULL, + "smokingAllowed" boolean DEFAULT false NOT NULL, + "petsAllowed" boolean DEFAULT false NOT NULL, + "quietHours" text, + "nearPublicTransport" boolean DEFAULT true NOT NULL, + "nearHealthServices" boolean DEFAULT false NOT NULL, + "nearSchools" boolean DEFAULT false NOT NULL, + "status" "HousingStatus" DEFAULT 'AVAILABLE' NOT NULL, + "notes" text, + "nickname" text, + "buildingCode" text +); +--> statement-breakpoint +CREATE TABLE "Incident" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "placementId" text, + "reportedById" text, + "subjectId" text, + "date" timestamp (3) NOT NULL, + "category" "IncidentCategory" DEFAULT 'INTERPERSONAL' NOT NULL, + "type" "IncidentType" NOT NULL, + "severity" "IncidentSeverity" NOT NULL, + "description" text NOT NULL, + "resolution" text, + "resolvedAt" timestamp (3), + "predictable" boolean, + "compatibilityGap" text, + "nextFollowUpDate" timestamp (3), + "followUpPriority" "FollowUpPriority", + "mediationMinutes" integer, + "resolutionStage" "ResolutionStage" DEFAULT 'REPORTED' NOT NULL, + "stageEnteredAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "IncidentFollowUp" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "incidentId" text NOT NULL, + "action" text NOT NULL, + "notes" text, + "outcome" text, + "staffName" text, + "scheduledNextDate" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "IncidentInvolvement" ( + "id" text PRIMARY KEY NOT NULL, + "incidentId" text NOT NULL, + "residentId" text NOT NULL, + "role" "InvolvementRole" DEFAULT 'INVOLVED' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "LearningRecord" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "kind" "LearningKind" NOT NULL, + "title" text NOT NULL, + "status" "LearningStatus" DEFAULT 'PLANNED' NOT NULL, + "languageCode" text, + "cefrLevel" text, + "provider" text, + "category" text, + "hours" integer, + "startedAt" timestamp (3), + "completedAt" timestamp (3), + "notes" text, + "recordedBy" "ResidentOrStaff" NOT NULL +); +--> statement-breakpoint +CREATE TABLE "MaintenanceRequest" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "spotId" text, + "category" "MaintenanceCategory" NOT NULL, + "priority" "MaintenancePriority" DEFAULT 'NORMAL' NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "location" text, + "reportedById" text, + "reporterName" text, + "assignedTo" text, + "assignedAt" timestamp (3), + "status" "MaintenanceStatus" DEFAULT 'OPEN' NOT NULL, + "startedAt" timestamp (3), + "completedAt" timestamp (3), + "resolution" text, + "cost" double precision, + "notes" text +); +--> statement-breakpoint +CREATE TABLE "MarketplacePost" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "postedById" text NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "kind" "MarketplacePostKind" NOT NULL, + "category" text DEFAULT 'OTHER' NOT NULL, + "status" "MarketplacePostStatus" DEFAULT 'OPEN' NOT NULL, + "claimedById" text, + "closedAt" timestamp (3), + "hiddenByStaff" boolean DEFAULT false NOT NULL, + "hiddenReason" text, + "contactNote" text, + "claimedAt" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "Message" ( + "id" text PRIMARY KEY NOT NULL, + "threadId" text NOT NULL, + "authorResidentId" text, + "authorUserId" text, + "body" text NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "readAt" timestamp (3), + CONSTRAINT "Message_one_author" CHECK (("authorResidentId" IS NOT NULL) <> ("authorUserId" IS NOT NULL)) +); +--> statement-breakpoint +CREATE TABLE "MessageThread" ( + "id" text PRIMARY KEY NOT NULL, + "residentId" text NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "Opportunity" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "kind" "OpportunityKind" NOT NULL, + "title" text NOT NULL, + "description" text NOT NULL, + "organisation" text NOT NULL, + "location" text, + "schedule" text, + "hoursPerWeek" integer, + "seats" integer, + "germanLevel" text, + "permitRequirement" "PermitRequirement" DEFAULT 'NONE' NOT NULL, + "requirementNote" text, + "contactName" text, + "contactEmail" text, + "contactPhone" text, + "website" text, + "status" "OpportunityStatus" DEFAULT 'DRAFT' NOT NULL, + "startsAt" timestamp (3), + "endsAt" timestamp (3), + "createdByUserId" text, + "updatedByUserId" text +); +--> statement-breakpoint +CREATE TABLE "OpportunityApplication" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "opportunityId" text NOT NULL, + "stage" "ApplicationStage" DEFAULT 'INTERESTED' NOT NULL, + "stageChangedAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "note" text, + "createdBy" "ResidentOrStaff" NOT NULL, + "supportedByUserId" text, + "learningRecordId" text +); +--> statement-breakpoint +CREATE TABLE "Placement" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "housingUnitId" text NOT NULL, + "spotId" text, + "startDate" timestamp (3) NOT NULL, + "endDate" timestamp (3), + "compatibilityScore" double precision, + "lifestyleScore" double precision, + "socialScore" double precision, + "practicalScore" double precision, + "riskScore" double precision, + "status" "PlacementStatus" DEFAULT 'ACTIVE' NOT NULL, + "endReason" "EndReason", + "satisfactionRating" integer, + "placementNotes" text, + "outcomeNotes" text, + "conflictGap" text, + "wasPredictable" boolean, + "relatedIncidentId" text +); +--> statement-breakpoint +CREATE TABLE "PlacementSpot" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "code" text NOT NULL, + "label" text, + "type" "SpotType" NOT NULL, + "parentSpotId" text, + "squareMeters" double precision, + "floor" integer, + "hasPrivateBathroom" boolean DEFAULT false NOT NULL, + "hasPrivateKitchen" boolean DEFAULT false NOT NULL, + "hasPrivateToilet" boolean DEFAULT false NOT NULL, + "capacity" integer DEFAULT 1 NOT NULL, + "requiresMedicalDocs" boolean DEFAULT false NOT NULL, + "status" "SpotStatus" DEFAULT 'AVAILABLE' NOT NULL, + "notes" text +); +--> statement-breakpoint +CREATE TABLE "Proposal" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "housingUnitId" text NOT NULL, + "type" "ProposalType" NOT NULL, + "category" "RuleCategory" NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "targetRuleId" text, + "parentOrgRuleId" text, + "proposedByResidentId" text, + "proposedByStaff" text, + "status" "ProposalStatus" DEFAULT 'DISCUSSION' NOT NULL, + "decisionMode" "DecisionMode" NOT NULL, + "threshold" "VoteThreshold" NOT NULL, + "quorumPercent" integer NOT NULL, + "approvalPercent" integer NOT NULL, + "eligibleVoterCount" integer DEFAULT 0 NOT NULL, + "discussionEndsAt" timestamp (3), + "votingOpenedAt" timestamp (3), + "votingEndsAt" timestamp (3), + "decidedAt" timestamp (3), + "outcomeSummary" text, + "staffDecision" "StaffDecision", + "staffNotes" text, + "staffUserId" text, + "staffDecidedAt" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "Resident" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "code" text NOT NULL, + "ageRange" "AgeRange" NOT NULL, + "gender" "Gender" NOT NULL, + "familyStatus" "FamilyStatus" NOT NULL, + "sleepSchedule" "SleepSchedule" NOT NULL, + "noiseTolerance" integer NOT NULL, + "cleanlinessPractice" integer NOT NULL, + "guestTolerance" integer DEFAULT 3 NOT NULL, + "socialStyle" "SocialStyle" NOT NULL, + "languages" text[], + "culturalRegion" text, + "conflictStyle" "ConflictStyle" DEFAULT 'COOPERATIVE' NOT NULL, + "smokingStatus" "SmokingStatus" NOT NULL, + "dietaryNeeds" text[], + "mobilityNeeds" "MobilityNeed" NOT NULL, + "medicalEquipment" boolean DEFAULT false NOT NULL, + "petTolerance" boolean DEFAULT true NOT NULL, + "sharedBathroom" boolean DEFAULT true NOT NULL, + "sharedKitchen" boolean DEFAULT true NOT NULL, + "privacyNeed" integer NOT NULL, + "choresContribution" integer DEFAULT 3 NOT NULL, + "recyclingKnowledge" "RecyclingKnowledge" DEFAULT 'NONE' NOT NULL, + "roomSharingStatus" "RoomSharingStatus" DEFAULT 'CAN_SHARE' NOT NULL, + "hasNightDisturbances" boolean DEFAULT false NOT NULL, + "needsQuietEnvironment" boolean DEFAULT false NOT NULL, + "hasSleepEquipment" boolean DEFAULT false NOT NULL, + "supportLevel" "SupportLevel" DEFAULT 'STANDARD' NOT NULL, + "roommatePreferences" text, + "status" "ResidentStatus" DEFAULT 'ACTIVE' NOT NULL, + "notes" text, + "hasMedicalDocumentation" boolean DEFAULT false NOT NULL, + "medicalDocType" "MedicalDocType", + "medicalDocDate" timestamp (3), + "medicalDocNotes" text, + "preferencesCompletedAt" timestamp (3), + "cleanlinessExpectation" integer DEFAULT 3 NOT NULL, + "chaosTolerance" integer DEFAULT 3 NOT NULL, + "bio" text, + "displayName" text, + "profileVisibility" "ProfileVisibility" DEFAULT 'ROOMMATES' NOT NULL, + "livingSkillsSupport" "LivingSkillsSupport" DEFAULT 'INDEPENDENT' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ResidentDocument" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "category" text DEFAULT 'OTHER' NOT NULL, + "title" text NOT NULL, + "fileName" text NOT NULL, + "mimeType" text NOT NULL, + "sizeBytes" integer NOT NULL, + "uploadedByUserId" text +); +--> statement-breakpoint +CREATE TABLE "ResidentDocumentBlob" ( + "documentId" text PRIMARY KEY NOT NULL, + "data" "bytea" NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ResidentPhoto" ( + "residentId" text PRIMARY KEY NOT NULL, + "data" "bytea" NOT NULL, + "mimeType" text NOT NULL, + "updatedAt" timestamp (3) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "RuleAcknowledgement" ( + "id" text PRIMARY KEY NOT NULL, + "ruleId" text NOT NULL, + "residentId" text NOT NULL, + "ruleVersion" integer NOT NULL, + "acknowledgedAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +CREATE TABLE "SatisfactionCheckIn" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "placementId" text NOT NULL, + "checkInType" "CheckInType" NOT NULL, + "weekNumber" integer, + "overallSatisfaction" integer NOT NULL, + "roommateRelations" integer, + "facilitySatisfaction" integer, + "safetyFeeling" integer, + "concerns" text, + "improvements" text, + "positives" text, + "collectedBy" text, + "isAnonymous" boolean DEFAULT false NOT NULL, + "appointmentId" text, + "collectedByUserId" text +); +--> statement-breakpoint +CREATE TABLE "Settlement" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "housingUnitId" text NOT NULL, + "fromId" text NOT NULL, + "toId" text NOT NULL, + "amountRappen" integer NOT NULL, + "note" text +); +--> statement-breakpoint +CREATE TABLE "SystemConfig" ( + "id" text PRIMARY KEY DEFAULT 'singleton' NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "pilotBaselineIncidentsPerMonth" double precision, + "pilotBaselineRelocationsPerMonth" double precision, + "pilotBaselineMediationHoursPerWeek" double precision, + "pilotStartDate" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "TaskAttentionFlag" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "taskId" text NOT NULL, + "flaggedById" text NOT NULL, + "message" text, + "isResolved" boolean DEFAULT false NOT NULL, + "resolvedAt" timestamp (3), + "resolvedByCompletionId" text +); +--> statement-breakpoint +CREATE TABLE "TaskCompletion" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "taskId" text NOT NULL, + "completedById" text NOT NULL, + "completedAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "notes" text, + "durationMinutes" integer, + "completedItems" text[] DEFAULT ARRAY[]::TEXT[] +); +--> statement-breakpoint +CREATE TABLE "TaskRequest" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "taskId" text NOT NULL, + "requestedById" text NOT NULL, + "requestedResidentId" text, + "isBroadcast" boolean DEFAULT false NOT NULL, + "message" text, + "status" "TaskRequestStatus" DEFAULT 'PENDING' NOT NULL, + "responseMessage" text, + "completionId" text +); +--> statement-breakpoint +CREATE TABLE "TransferRequest" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "residentId" text NOT NULL, + "currentPlacementId" text, + "targetUnitId" text, + "reason" text NOT NULL, + "status" "TransferRequestStatus" DEFAULT 'PENDING' NOT NULL, + "staffNotes" text, + "reviewedBy" text, + "reviewedAt" timestamp (3) +); +--> statement-breakpoint +CREATE TABLE "User" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" timestamp (3) NOT NULL, + "name" text NOT NULL, + "role" "StaffRole" DEFAULT 'BETREUUNG' NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "lastLoginAt" timestamp (3), + "code" text NOT NULL, + "scope" "StaffScope" DEFAULT 'OWN_DOMAIN' NOT NULL, + "isSystemAdmin" boolean DEFAULT false NOT NULL, + CONSTRAINT "User_code_key" UNIQUE("code") +); +--> statement-breakpoint +CREATE TABLE "Vote" ( + "id" text PRIMARY KEY NOT NULL, + "proposalId" text NOT NULL, + "residentId" text NOT NULL, + "choice" "VoteChoice" NOT NULL, + "reason" text, + "castAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL +); +--> statement-breakpoint +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Account" ADD CONSTRAINT "Account_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Activity" ADD CONSTRAINT "Activity_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "AgreementParty" ADD CONSTRAINT "AgreementParty_agreementId_fkey" FOREIGN KEY ("agreementId") REFERENCES "public"."ConflictAgreement"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "AgreementParty" ADD CONSTRAINT "AgreementParty_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "public"."Account"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CareAssignment" ADD CONSTRAINT "CareAssignment_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CareAssignment" ADD CONSTRAINT "CareAssignment_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CareAttribute" ADD CONSTRAINT "CareAttribute_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CareAttribute" ADD CONSTRAINT "CareAttribute_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CompatibilityAssessment" ADD CONSTRAINT "CompatibilityAssessment_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "CompatibilityAssessment" ADD CONSTRAINT "CompatibilityAssessment_comparedWithId_fkey" FOREIGN KEY ("comparedWithId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Complaint" ADD CONSTRAINT "Complaint_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Complaint" ADD CONSTRAINT "Complaint_respondedByUserId_fkey" FOREIGN KEY ("respondedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ConflictAgreement" ADD CONSTRAINT "ConflictAgreement_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "public"."Incident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ConflictAgreement" ADD CONSTRAINT "ConflictAgreement_ruleProposalId_fkey" FOREIGN KEY ("ruleProposalId") REFERENCES "public"."Proposal"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "EventRsvp" ADD CONSTRAINT "EventRsvp_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "public"."HouseEvent"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "EventRsvp" ADD CONSTRAINT "EventRsvp_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Expense" ADD CONSTRAINT "Expense_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Expense" ADD CONSTRAINT "Expense_paidById_fkey" FOREIGN KEY ("paidById") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Expense" ADD CONSTRAINT "Expense_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ExpenseShare" ADD CONSTRAINT "ExpenseShare_expenseId_fkey" FOREIGN KEY ("expenseId") REFERENCES "public"."Expense"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ExpenseShare" ADD CONSTRAINT "ExpenseShare_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_createdByStaffId_fkey" FOREIGN KEY ("createdByStaffId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_createdByResidentId_fkey" FOREIGN KEY ("createdByResidentId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_parentRuleId_fkey" FOREIGN KEY ("parentRuleId") REFERENCES "public"."HouseRule"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_adoptedByProposalId_fkey" FOREIGN KEY ("adoptedByProposalId") REFERENCES "public"."Proposal"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseholdTask" ADD CONSTRAINT "HouseholdTask_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "HouseholdTask" ADD CONSTRAINT "HouseholdTask_createdByResidentId_fkey" FOREIGN KEY ("createdByResidentId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Incident" ADD CONSTRAINT "Incident_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Incident" ADD CONSTRAINT "Incident_placementId_fkey" FOREIGN KEY ("placementId") REFERENCES "public"."Placement"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Incident" ADD CONSTRAINT "Incident_reportedById_fkey" FOREIGN KEY ("reportedById") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Incident" ADD CONSTRAINT "Incident_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "IncidentFollowUp" ADD CONSTRAINT "IncidentFollowUp_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "public"."Incident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "IncidentInvolvement" ADD CONSTRAINT "IncidentInvolvement_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "public"."Incident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "IncidentInvolvement" ADD CONSTRAINT "IncidentInvolvement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "LearningRecord" ADD CONSTRAINT "LearningRecord_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_spotId_fkey" FOREIGN KEY ("spotId") REFERENCES "public"."PlacementSpot"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_reportedById_fkey" FOREIGN KEY ("reportedById") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Message" ADD CONSTRAINT "Message_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "public"."MessageThread"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Message" ADD CONSTRAINT "Message_authorResidentId_fkey" FOREIGN KEY ("authorResidentId") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Message" ADD CONSTRAINT "Message_authorUserId_fkey" FOREIGN KEY ("authorUserId") REFERENCES "public"."User"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "MessageThread" ADD CONSTRAINT "MessageThread_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_opportunityId_fkey" FOREIGN KEY ("opportunityId") REFERENCES "public"."Opportunity"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_supportedByUserId_fkey" FOREIGN KEY ("supportedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_learningRecordId_fkey" FOREIGN KEY ("learningRecordId") REFERENCES "public"."LearningRecord"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Placement" ADD CONSTRAINT "Placement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Placement" ADD CONSTRAINT "Placement_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Placement" ADD CONSTRAINT "Placement_spotId_fkey" FOREIGN KEY ("spotId") REFERENCES "public"."PlacementSpot"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Placement" ADD CONSTRAINT "Placement_relatedIncidentId_fkey" FOREIGN KEY ("relatedIncidentId") REFERENCES "public"."Incident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "PlacementSpot" ADD CONSTRAINT "PlacementSpot_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "PlacementSpot" ADD CONSTRAINT "PlacementSpot_parentSpotId_fkey" FOREIGN KEY ("parentSpotId") REFERENCES "public"."PlacementSpot"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_targetRuleId_fkey" FOREIGN KEY ("targetRuleId") REFERENCES "public"."HouseRule"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_parentOrgRuleId_fkey" FOREIGN KEY ("parentOrgRuleId") REFERENCES "public"."HouseRule"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_proposedByResidentId_fkey" FOREIGN KEY ("proposedByResidentId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ResidentDocument" ADD CONSTRAINT "ResidentDocument_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ResidentDocument" ADD CONSTRAINT "ResidentDocument_uploadedByUserId_fkey" FOREIGN KEY ("uploadedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ResidentDocumentBlob" ADD CONSTRAINT "ResidentDocumentBlob_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "public"."ResidentDocument"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ResidentPhoto" ADD CONSTRAINT "ResidentPhoto_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "RuleAcknowledgement" ADD CONSTRAINT "RuleAcknowledgement_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "public"."HouseRule"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "RuleAcknowledgement" ADD CONSTRAINT "RuleAcknowledgement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_placementId_fkey" FOREIGN KEY ("placementId") REFERENCES "public"."Placement"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_appointmentId_fkey" FOREIGN KEY ("appointmentId") REFERENCES "public"."Appointment"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_collectedByUserId_fkey" FOREIGN KEY ("collectedByUserId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_toId_fkey" FOREIGN KEY ("toId") REFERENCES "public"."Resident"("id") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."HouseholdTask"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_flaggedById_fkey" FOREIGN KEY ("flaggedById") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_resolvedByCompletionId_fkey" FOREIGN KEY ("resolvedByCompletionId") REFERENCES "public"."TaskCompletion"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskCompletion" ADD CONSTRAINT "TaskCompletion_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."HouseholdTask"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskCompletion" ADD CONSTRAINT "TaskCompletion_completedById_fkey" FOREIGN KEY ("completedById") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "public"."HouseholdTask"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_requestedResidentId_fkey" FOREIGN KEY ("requestedResidentId") REFERENCES "public"."Resident"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_completionId_fkey" FOREIGN KEY ("completionId") REFERENCES "public"."TaskCompletion"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_currentPlacementId_fkey" FOREIGN KEY ("currentPlacementId") REFERENCES "public"."Placement"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_targetUnitId_fkey" FOREIGN KEY ("targetUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Vote" ADD CONSTRAINT "Vote_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "public"."Proposal"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Vote" ADD CONSTRAINT "Vote_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "public"."Resident"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +CREATE UNIQUE INDEX "Account_email_key" ON "Account" USING btree ("email");--> statement-breakpoint +CREATE INDEX "Account_residentId_idx" ON "Account" USING btree ("residentId");--> statement-breakpoint +CREATE UNIQUE INDEX "Account_residentId_key" ON "Account" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "Account_userId_idx" ON "Account" USING btree ("userId");--> statement-breakpoint +CREATE UNIQUE INDEX "Account_userId_key" ON "Account" USING btree ("userId");--> statement-breakpoint +CREATE INDEX "Activity_endsAt_idx" ON "Activity" USING btree ("endsAt");--> statement-breakpoint +CREATE INDEX "Activity_status_category_idx" ON "Activity" USING btree ("status","category");--> statement-breakpoint +CREATE INDEX "Activity_status_highlight_idx" ON "Activity" USING btree ("status","highlight");--> statement-breakpoint +CREATE UNIQUE INDEX "AgreementParty_agreementId_residentId_key" ON "AgreementParty" USING btree ("agreementId","residentId");--> statement-breakpoint +CREATE INDEX "AgreementParty_residentId_idx" ON "AgreementParty" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "AlgorithmWeight_active_idx" ON "AlgorithmWeight" USING btree ("active");--> statement-breakpoint +CREATE INDEX "Appointment_residentId_startsAt_idx" ON "Appointment" USING btree ("residentId","startsAt");--> statement-breakpoint +CREATE INDEX "Appointment_staffId_startsAt_idx" ON "Appointment" USING btree ("staffId","startsAt");--> statement-breakpoint +CREATE INDEX "Appointment_status_domain_idx" ON "Appointment" USING btree ("status","domain");--> statement-breakpoint +CREATE INDEX "Appointment_status_idx" ON "Appointment" USING btree ("status");--> statement-breakpoint +CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog" USING btree ("createdAt");--> statement-breakpoint +CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog" USING btree ("entity","entityId");--> statement-breakpoint +CREATE INDEX "AuditLog_userId_idx" ON "AuditLog" USING btree ("userId");--> statement-breakpoint +CREATE INDEX "AuthToken_accountId_purpose_idx" ON "AuthToken" USING btree ("accountId","purpose");--> statement-breakpoint +CREATE UNIQUE INDEX "AuthToken_tokenHash_key" ON "AuthToken" USING btree ("tokenHash");--> statement-breakpoint +CREATE UNIQUE INDEX "CareAssignment_residentId_role_key" ON "CareAssignment" USING btree ("residentId","role");--> statement-breakpoint +CREATE INDEX "CareAssignment_staffId_idx" ON "CareAssignment" USING btree ("staffId");--> statement-breakpoint +CREATE INDEX "CareAttribute_residentId_domain_idx" ON "CareAttribute" USING btree ("residentId","domain");--> statement-breakpoint +CREATE UNIQUE INDEX "CareAttribute_residentId_domain_key_key" ON "CareAttribute" USING btree ("residentId","domain","key");--> statement-breakpoint +CREATE INDEX "CompatibilityAssessment_overallScore_idx" ON "CompatibilityAssessment" USING btree ("overallScore");--> statement-breakpoint +CREATE UNIQUE INDEX "CompatibilityAssessment_residentId_comparedWithId_key" ON "CompatibilityAssessment" USING btree ("residentId","comparedWithId");--> statement-breakpoint +CREATE INDEX "Complaint_createdAt_idx" ON "Complaint" USING btree ("createdAt");--> statement-breakpoint +CREATE INDEX "Complaint_residentId_idx" ON "Complaint" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "Complaint_status_idx" ON "Complaint" USING btree ("status");--> statement-breakpoint +CREATE INDEX "ConflictAgreement_incidentId_idx" ON "ConflictAgreement" USING btree ("incidentId");--> statement-breakpoint +CREATE UNIQUE INDEX "ConflictAgreement_ruleProposalId_key" ON "ConflictAgreement" USING btree ("ruleProposalId");--> statement-breakpoint +CREATE INDEX "ConflictAgreement_status_reviewDate_idx" ON "ConflictAgreement" USING btree ("status","reviewDate");--> statement-breakpoint +CREATE INDEX "EventRsvp_eventId_idx" ON "EventRsvp" USING btree ("eventId");--> statement-breakpoint +CREATE UNIQUE INDEX "EventRsvp_eventId_residentId_key" ON "EventRsvp" USING btree ("eventId","residentId");--> statement-breakpoint +CREATE INDEX "Expense_housingUnitId_date_idx" ON "Expense" USING btree ("housingUnitId","date");--> statement-breakpoint +CREATE UNIQUE INDEX "ExpenseShare_expenseId_residentId_key" ON "ExpenseShare" USING btree ("expenseId","residentId");--> statement-breakpoint +CREATE INDEX "ExpenseShare_residentId_idx" ON "ExpenseShare" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "HouseEvent_housingUnitId_startsAt_idx" ON "HouseEvent" USING btree ("housingUnitId","startsAt");--> statement-breakpoint +CREATE INDEX "HouseEvent_status_startsAt_idx" ON "HouseEvent" USING btree ("status","startsAt");--> statement-breakpoint +CREATE INDEX "HouseRule_category_idx" ON "HouseRule" USING btree ("category");--> statement-breakpoint +CREATE INDEX "HouseRule_housingUnitId_status_idx" ON "HouseRule" USING btree ("housingUnitId","status");--> statement-breakpoint +CREATE UNIQUE INDEX "HouseRule_key_key" ON "HouseRule" USING btree ("key");--> statement-breakpoint +CREATE INDEX "HouseRule_parentRuleId_idx" ON "HouseRule" USING btree ("parentRuleId");--> statement-breakpoint +CREATE INDEX "HouseRule_scope_status_idx" ON "HouseRule" USING btree ("scope","status");--> statement-breakpoint +CREATE INDEX "HouseholdTask_housingUnitId_category_idx" ON "HouseholdTask" USING btree ("housingUnitId","category");--> statement-breakpoint +CREATE INDEX "HouseholdTask_housingUnitId_currentStatus_idx" ON "HouseholdTask" USING btree ("housingUnitId","currentStatus");--> statement-breakpoint +CREATE INDEX "HousingUnit_buildingCode_idx" ON "HousingUnit" USING btree ("buildingCode");--> statement-breakpoint +CREATE UNIQUE INDEX "HousingUnit_code_key" ON "HousingUnit" USING btree ("code");--> statement-breakpoint +CREATE INDEX "HousingUnit_status_idx" ON "HousingUnit" USING btree ("status");--> statement-breakpoint +CREATE INDEX "HousingUnit_totalBeds_idx" ON "HousingUnit" USING btree ("totalBeds");--> statement-breakpoint +CREATE INDEX "Incident_date_idx" ON "Incident" USING btree ("date");--> statement-breakpoint +CREATE INDEX "Incident_nextFollowUpDate_idx" ON "Incident" USING btree ("nextFollowUpDate");--> statement-breakpoint +CREATE INDEX "Incident_reportedById_idx" ON "Incident" USING btree ("reportedById");--> statement-breakpoint +CREATE INDEX "Incident_subjectId_idx" ON "Incident" USING btree ("subjectId");--> statement-breakpoint +CREATE INDEX "Incident_type_severity_idx" ON "Incident" USING btree ("type","severity");--> statement-breakpoint +CREATE INDEX "IncidentFollowUp_createdAt_idx" ON "IncidentFollowUp" USING btree ("createdAt");--> statement-breakpoint +CREATE INDEX "IncidentFollowUp_incidentId_idx" ON "IncidentFollowUp" USING btree ("incidentId");--> statement-breakpoint +CREATE UNIQUE INDEX "IncidentInvolvement_incidentId_residentId_key" ON "IncidentInvolvement" USING btree ("incidentId","residentId");--> statement-breakpoint +CREATE INDEX "IncidentInvolvement_residentId_idx" ON "IncidentInvolvement" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "LearningRecord_languageCode_cefrLevel_idx" ON "LearningRecord" USING btree ("languageCode","cefrLevel");--> statement-breakpoint +CREATE INDEX "LearningRecord_residentId_kind_idx" ON "LearningRecord" USING btree ("residentId","kind");--> statement-breakpoint +CREATE INDEX "LearningRecord_status_idx" ON "LearningRecord" USING btree ("status");--> statement-breakpoint +CREATE INDEX "MaintenanceRequest_createdAt_idx" ON "MaintenanceRequest" USING btree ("createdAt");--> statement-breakpoint +CREATE INDEX "MaintenanceRequest_housingUnitId_idx" ON "MaintenanceRequest" USING btree ("housingUnitId");--> statement-breakpoint +CREATE INDEX "MaintenanceRequest_priority_status_idx" ON "MaintenanceRequest" USING btree ("priority","status");--> statement-breakpoint +CREATE INDEX "MaintenanceRequest_reportedById_idx" ON "MaintenanceRequest" USING btree ("reportedById");--> statement-breakpoint +CREATE INDEX "MaintenanceRequest_status_idx" ON "MaintenanceRequest" USING btree ("status");--> statement-breakpoint +CREATE INDEX "MarketplacePost_housingUnitId_status_idx" ON "MarketplacePost" USING btree ("housingUnitId","status");--> statement-breakpoint +CREATE INDEX "MarketplacePost_postedById_idx" ON "MarketplacePost" USING btree ("postedById");--> statement-breakpoint +CREATE INDEX "Message_threadId_createdAt_idx" ON "Message" USING btree ("threadId","createdAt");--> statement-breakpoint +CREATE UNIQUE INDEX "MessageThread_residentId_key" ON "MessageThread" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "MessageThread_updatedAt_idx" ON "MessageThread" USING btree ("updatedAt");--> statement-breakpoint +CREATE INDEX "Opportunity_endsAt_idx" ON "Opportunity" USING btree ("endsAt");--> statement-breakpoint +CREATE INDEX "Opportunity_status_kind_idx" ON "Opportunity" USING btree ("status","kind");--> statement-breakpoint +CREATE UNIQUE INDEX "OpportunityApplication_learningRecordId_key" ON "OpportunityApplication" USING btree ("learningRecordId");--> statement-breakpoint +CREATE INDEX "OpportunityApplication_opportunityId_stage_idx" ON "OpportunityApplication" USING btree ("opportunityId","stage");--> statement-breakpoint +CREATE INDEX "OpportunityApplication_residentId_idx" ON "OpportunityApplication" USING btree ("residentId");--> statement-breakpoint +CREATE UNIQUE INDEX "OpportunityApplication_residentId_opportunityId_key" ON "OpportunityApplication" USING btree ("residentId","opportunityId");--> statement-breakpoint +CREATE INDEX "OpportunityApplication_stage_idx" ON "OpportunityApplication" USING btree ("stage");--> statement-breakpoint +CREATE INDEX "Placement_housingUnitId_idx" ON "Placement" USING btree ("housingUnitId");--> statement-breakpoint +CREATE UNIQUE INDEX "Placement_residentId_housingUnitId_startDate_key" ON "Placement" USING btree ("residentId","housingUnitId","startDate");--> statement-breakpoint +CREATE INDEX "Placement_residentId_idx" ON "Placement" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "Placement_startDate_endDate_idx" ON "Placement" USING btree ("startDate","endDate");--> statement-breakpoint +CREATE INDEX "Placement_status_idx" ON "Placement" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "PlacementSpot_housingUnitId_code_key" ON "PlacementSpot" USING btree ("housingUnitId","code");--> statement-breakpoint +CREATE INDEX "PlacementSpot_housingUnitId_idx" ON "PlacementSpot" USING btree ("housingUnitId");--> statement-breakpoint +CREATE INDEX "PlacementSpot_requiresMedicalDocs_idx" ON "PlacementSpot" USING btree ("requiresMedicalDocs");--> statement-breakpoint +CREATE INDEX "PlacementSpot_type_status_idx" ON "PlacementSpot" USING btree ("type","status");--> statement-breakpoint +CREATE INDEX "Proposal_housingUnitId_status_idx" ON "Proposal" USING btree ("housingUnitId","status");--> statement-breakpoint +CREATE INDEX "Proposal_status_votingEndsAt_idx" ON "Proposal" USING btree ("status","votingEndsAt");--> statement-breakpoint +CREATE INDEX "Resident_ageRange_gender_idx" ON "Resident" USING btree ("ageRange","gender");--> statement-breakpoint +CREATE UNIQUE INDEX "Resident_code_key" ON "Resident" USING btree ("code");--> statement-breakpoint +CREATE INDEX "Resident_livingSkillsSupport_idx" ON "Resident" USING btree ("livingSkillsSupport");--> statement-breakpoint +CREATE INDEX "Resident_status_idx" ON "Resident" USING btree ("status");--> statement-breakpoint +CREATE INDEX "ResidentDocument_residentId_createdAt_idx" ON "ResidentDocument" USING btree ("residentId","createdAt");--> statement-breakpoint +CREATE INDEX "RuleAcknowledgement_residentId_idx" ON "RuleAcknowledgement" USING btree ("residentId");--> statement-breakpoint +CREATE UNIQUE INDEX "RuleAcknowledgement_ruleId_residentId_ruleVersion_key" ON "RuleAcknowledgement" USING btree ("ruleId","residentId","ruleVersion");--> statement-breakpoint +CREATE UNIQUE INDEX "SatisfactionCheckIn_appointmentId_key" ON "SatisfactionCheckIn" USING btree ("appointmentId");--> statement-breakpoint +CREATE INDEX "SatisfactionCheckIn_checkInType_idx" ON "SatisfactionCheckIn" USING btree ("checkInType");--> statement-breakpoint +CREATE INDEX "SatisfactionCheckIn_collectedByUserId_idx" ON "SatisfactionCheckIn" USING btree ("collectedByUserId");--> statement-breakpoint +CREATE INDEX "SatisfactionCheckIn_placementId_idx" ON "SatisfactionCheckIn" USING btree ("placementId");--> statement-breakpoint +CREATE INDEX "Settlement_housingUnitId_idx" ON "Settlement" USING btree ("housingUnitId");--> statement-breakpoint +CREATE INDEX "TaskAttentionFlag_taskId_idx" ON "TaskAttentionFlag" USING btree ("taskId");--> statement-breakpoint +CREATE INDEX "TaskCompletion_completedById_idx" ON "TaskCompletion" USING btree ("completedById");--> statement-breakpoint +CREATE INDEX "TaskCompletion_taskId_idx" ON "TaskCompletion" USING btree ("taskId");--> statement-breakpoint +CREATE INDEX "TaskRequest_requestedResidentId_idx" ON "TaskRequest" USING btree ("requestedResidentId");--> statement-breakpoint +CREATE INDEX "TaskRequest_taskId_idx" ON "TaskRequest" USING btree ("taskId");--> statement-breakpoint +CREATE INDEX "TransferRequest_residentId_idx" ON "TransferRequest" USING btree ("residentId");--> statement-breakpoint +CREATE INDEX "TransferRequest_status_idx" ON "TransferRequest" USING btree ("status");--> statement-breakpoint +CREATE INDEX "User_code_idx" ON "User" USING btree ("code");--> statement-breakpoint +CREATE INDEX "User_role_idx" ON "User" USING btree ("role");--> statement-breakpoint +CREATE INDEX "User_scope_idx" ON "User" USING btree ("scope");--> statement-breakpoint +CREATE UNIQUE INDEX "Vote_proposalId_residentId_key" ON "Vote" USING btree ("proposalId","residentId");--> statement-breakpoint +CREATE INDEX "Vote_residentId_idx" ON "Vote" USING btree ("residentId"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..f456bf11 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,8152 @@ +{ + "id": "e44fa51c-d34d-4880-a554-e3551fc5c9f2", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerifiedAt": { + "name": "emailVerifiedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_email_key": { + "name": "Account_email_key", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_idx": { + "name": "Account_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_key": { + "name": "Account_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_idx": { + "name": "Account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_key": { + "name": "Account_userId_key", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Account_userId_fkey": { + "name": "Account_userId_fkey", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Account_residentId_fkey": { + "name": "Account_residentId_fkey", + "tableFrom": "Account", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Activity": { + "name": "Activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "ActivityCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "ActivityCost", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FREE'" + }, + "costNote": { + "name": "costNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ActivityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "highlight": { + "name": "highlight", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Activity_endsAt_idx": { + "name": "Activity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_category_idx": { + "name": "Activity_status_category_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_highlight_idx": { + "name": "Activity_status_highlight_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "highlight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Activity_createdByUserId_fkey": { + "name": "Activity_createdByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": [ + "createdByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Activity_updatedByUserId_fkey": { + "name": "Activity_updatedByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": [ + "updatedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AgreementParty": { + "name": "AgreementParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agreementId": { + "name": "agreementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "declinedAt": { + "name": "declinedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AgreementParty_agreementId_residentId_key": { + "name": "AgreementParty_agreementId_residentId_key", + "columns": [ + { + "expression": "agreementId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AgreementParty_residentId_idx": { + "name": "AgreementParty_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AgreementParty_agreementId_fkey": { + "name": "AgreementParty_agreementId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "ConflictAgreement", + "columnsFrom": [ + "agreementId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "AgreementParty_residentId_fkey": { + "name": "AgreementParty_residentId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AlgorithmWeight": { + "name": "AlgorithmWeight", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "lifestyleWeight": { + "name": "lifestyleWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "socialWeight": { + "name": "socialWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "practicalWeight": { + "name": "practicalWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "riskWeight": { + "name": "riskWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "factorWeights": { + "name": "factorWeights", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AlgorithmWeight_active_idx": { + "name": "AlgorithmWeight_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Appointment": { + "name": "Appointment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "AppointmentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SCHEDULED'" + }, + "residentNote": { + "name": "residentNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffNote": { + "name": "staffNote", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Appointment_residentId_startsAt_idx": { + "name": "Appointment_residentId_startsAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_staffId_startsAt_idx": { + "name": "Appointment_staffId_startsAt_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_domain_idx": { + "name": "Appointment_status_domain_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_idx": { + "name": "Appointment_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Appointment_residentId_fkey": { + "name": "Appointment_residentId_fkey", + "tableFrom": "Appointment", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Appointment_staffId_fkey": { + "name": "Appointment_staffId_fkey", + "tableFrom": "Appointment", + "tableTo": "User", + "columnsFrom": [ + "staffId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuditLog": { + "name": "AuditLog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entityId": { + "name": "entityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AuditLog_createdAt_idx": { + "name": "AuditLog_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_entity_entityId_idx": { + "name": "AuditLog_entity_entityId_idx", + "columns": [ + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_userId_idx": { + "name": "AuditLog_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuditLog_userId_fkey": { + "name": "AuditLog_userId_fkey", + "tableFrom": "AuditLog", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuthToken": { + "name": "AuthToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "AuthTokenPurpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "usedAt": { + "name": "usedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "AuthToken_accountId_purpose_idx": { + "name": "AuthToken_accountId_purpose_idx", + "columns": [ + { + "expression": "accountId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purpose", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuthToken_tokenHash_key": { + "name": "AuthToken_tokenHash_key", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuthToken_accountId_fkey": { + "name": "AuthToken_accountId_fkey", + "tableFrom": "AuthToken", + "tableTo": "Account", + "columnsFrom": [ + "accountId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAssignment": { + "name": "CareAssignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAssignment_residentId_role_key": { + "name": "CareAssignment_residentId_role_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAssignment_staffId_idx": { + "name": "CareAssignment_staffId_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAssignment_residentId_fkey": { + "name": "CareAssignment_residentId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAssignment_staffId_fkey": { + "name": "CareAssignment_staffId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "User", + "columnsFrom": [ + "staffId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAttribute": { + "name": "CareAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedById": { + "name": "updatedById", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAttribute_residentId_domain_idx": { + "name": "CareAttribute_residentId_domain_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAttribute_residentId_domain_key_key": { + "name": "CareAttribute_residentId_domain_key_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAttribute_residentId_fkey": { + "name": "CareAttribute_residentId_fkey", + "tableFrom": "CareAttribute", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAttribute_updatedById_fkey": { + "name": "CareAttribute_updatedById_fkey", + "tableFrom": "CareAttribute", + "tableTo": "User", + "columnsFrom": [ + "updatedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CompatibilityAssessment": { + "name": "CompatibilityAssessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparedWithId": { + "name": "comparedWithId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overallScore": { + "name": "overallScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "strengths": { + "name": "strengths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "recommendations": { + "name": "recommendations", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "CompatibilityAssessment_overallScore_idx": { + "name": "CompatibilityAssessment_overallScore_idx", + "columns": [ + { + "expression": "overallScore", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CompatibilityAssessment_residentId_comparedWithId_key": { + "name": "CompatibilityAssessment_residentId_comparedWithId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "comparedWithId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CompatibilityAssessment_residentId_fkey": { + "name": "CompatibilityAssessment_residentId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CompatibilityAssessment_comparedWithId_fkey": { + "name": "CompatibilityAssessment_comparedWithId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": [ + "comparedWithId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Complaint": { + "name": "Complaint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "ComplaintSubject", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ComplaintStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "respondedAt": { + "name": "respondedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "respondedByUserId": { + "name": "respondedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Complaint_createdAt_idx": { + "name": "Complaint_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_residentId_idx": { + "name": "Complaint_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_status_idx": { + "name": "Complaint_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Complaint_residentId_fkey": { + "name": "Complaint_residentId_fkey", + "tableFrom": "Complaint", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Complaint_respondedByUserId_fkey": { + "name": "Complaint_respondedByUserId_fkey", + "tableFrom": "Complaint", + "tableTo": "User", + "columnsFrom": [ + "respondedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ConflictAgreement": { + "name": "ConflictAgreement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms": { + "name": "terms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mediatorName": { + "name": "mediatorName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewDate": { + "name": "reviewDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "AgreementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PROPOSED'" + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "ruleProposalId": { + "name": "ruleProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ConflictAgreement_incidentId_idx": { + "name": "ConflictAgreement_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_ruleProposalId_key": { + "name": "ConflictAgreement_ruleProposalId_key", + "columns": [ + { + "expression": "ruleProposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_status_reviewDate_idx": { + "name": "ConflictAgreement_status_reviewDate_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reviewDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ConflictAgreement_incidentId_fkey": { + "name": "ConflictAgreement_incidentId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Incident", + "columnsFrom": [ + "incidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ConflictAgreement_ruleProposalId_fkey": { + "name": "ConflictAgreement_ruleProposalId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Proposal", + "columnsFrom": [ + "ruleProposalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EventRsvp": { + "name": "EventRsvp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "EventRsvpStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GOING'" + } + }, + "indexes": { + "EventRsvp_eventId_idx": { + "name": "EventRsvp_eventId_idx", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EventRsvp_eventId_residentId_key": { + "name": "EventRsvp_eventId_residentId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EventRsvp_eventId_fkey": { + "name": "EventRsvp_eventId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "HouseEvent", + "columnsFrom": [ + "eventId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EventRsvp_residentId_fkey": { + "name": "EventRsvp_residentId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Expense": { + "name": "Expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paidById": { + "name": "paidById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdById": { + "name": "createdById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Expense_housingUnitId_date_idx": { + "name": "Expense_housingUnitId_date_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Expense_housingUnitId_fkey": { + "name": "Expense_housingUnitId_fkey", + "tableFrom": "Expense", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Expense_paidById_fkey": { + "name": "Expense_paidById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": [ + "paidById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Expense_createdById_fkey": { + "name": "Expense_createdById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": [ + "createdById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ExpenseShare": { + "name": "ExpenseShare", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expenseId": { + "name": "expenseId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ExpenseShare_expenseId_residentId_key": { + "name": "ExpenseShare_expenseId_residentId_key", + "columns": [ + { + "expression": "expenseId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ExpenseShare_residentId_idx": { + "name": "ExpenseShare_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ExpenseShare_expenseId_fkey": { + "name": "ExpenseShare_expenseId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Expense", + "columnsFrom": [ + "expenseId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ExpenseShare_residentId_fkey": { + "name": "ExpenseShare_residentId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseEvent": { + "name": "HouseEvent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "HouseEventCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SOCIAL'" + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "HouseEventStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLISHED'" + }, + "createdByStaffId": { + "name": "createdByStaffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseEvent_housingUnitId_startsAt_idx": { + "name": "HouseEvent_housingUnitId_startsAt_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseEvent_status_startsAt_idx": { + "name": "HouseEvent_status_startsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseEvent_housingUnitId_fkey": { + "name": "HouseEvent_housingUnitId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseEvent_createdByStaffId_fkey": { + "name": "HouseEvent_createdByStaffId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "User", + "columnsFrom": [ + "createdByStaffId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "HouseEvent_createdByResidentId_fkey": { + "name": "HouseEvent_createdByResidentId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "Resident", + "columnsFrom": [ + "createdByResidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseRule": { + "name": "HouseRule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "RuleScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegation": { + "name": "delegation", + "type": "RuleDelegation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FIXED'" + }, + "parentRuleId": { + "name": "parentRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "RuleStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "effectiveFrom": { + "name": "effectiveFrom", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "effectiveUntil": { + "name": "effectiveUntil", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "adoptedByProposalId": { + "name": "adoptedByProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseRule_category_idx": { + "name": "HouseRule_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_housingUnitId_status_idx": { + "name": "HouseRule_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_key_key": { + "name": "HouseRule_key_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_parentRuleId_idx": { + "name": "HouseRule_parentRuleId_idx", + "columns": [ + { + "expression": "parentRuleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_scope_status_idx": { + "name": "HouseRule_scope_status_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseRule_housingUnitId_fkey": { + "name": "HouseRule_housingUnitId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseRule_parentRuleId_fkey": { + "name": "HouseRule_parentRuleId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HouseRule", + "columnsFrom": [ + "parentRuleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "HouseRule_adoptedByProposalId_fkey": { + "name": "HouseRule_adoptedByProposalId_fkey", + "tableFrom": "HouseRule", + "tableTo": "Proposal", + "columnsFrom": [ + "adoptedByProposalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseholdTask": { + "name": "HouseholdTask", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taskType": { + "name": "taskType", + "type": "HouseholdTaskType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ONE_TIME'" + }, + "category": { + "name": "category", + "type": "HouseholdTaskCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "priority": { + "name": "priority", + "type": "HouseholdTaskPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "scheduleHuman": { + "name": "scheduleHuman", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimatedMinutes": { + "name": "estimatedMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currentStatus": { + "name": "currentStatus", + "type": "HouseholdTaskStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'IDLE'" + }, + "isCompleted": { + "name": "isCompleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checklist": { + "name": "checklist", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + }, + "rotationResidentIds": { + "name": "rotationResidentIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "HouseholdTask_housingUnitId_category_idx": { + "name": "HouseholdTask_housingUnitId_category_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseholdTask_housingUnitId_currentStatus_idx": { + "name": "HouseholdTask_housingUnitId_currentStatus_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currentStatus", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseholdTask_housingUnitId_fkey": { + "name": "HouseholdTask_housingUnitId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseholdTask_createdByResidentId_fkey": { + "name": "HouseholdTask_createdByResidentId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "Resident", + "columnsFrom": [ + "createdByResidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HousingUnit": { + "name": "HousingUnit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totalBeds": { + "name": "totalBeds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "totalRooms": { + "name": "totalRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedRooms": { + "name": "sharedRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateRooms": { + "name": "privateRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedBathrooms": { + "name": "sharedBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateBathrooms": { + "name": "privateBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privateKitchen": { + "name": "privateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "groundFloor": { + "name": "groundFloor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "wheelchairAccess": { + "name": "wheelchairAccess", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elevator": { + "name": "elevator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "smokingAllowed": { + "name": "smokingAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petsAllowed": { + "name": "petsAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "quietHours": { + "name": "quietHours", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nearPublicTransport": { + "name": "nearPublicTransport", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "nearHealthServices": { + "name": "nearHealthServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nearSchools": { + "name": "nearSchools", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "HousingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildingCode": { + "name": "buildingCode", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HousingUnit_buildingCode_idx": { + "name": "HousingUnit_buildingCode_idx", + "columns": [ + { + "expression": "buildingCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_code_key": { + "name": "HousingUnit_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_status_idx": { + "name": "HousingUnit_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_totalBeds_idx": { + "name": "HousingUnit_totalBeds_idx", + "columns": [ + { + "expression": "totalBeds", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Incident": { + "name": "Incident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subjectId": { + "name": "subjectId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "IncidentCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERPERSONAL'" + }, + "type": { + "name": "type", + "type": "IncidentType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "IncidentSeverity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "predictable": { + "name": "predictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compatibilityGap": { + "name": "compatibilityGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nextFollowUpDate": { + "name": "nextFollowUpDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "followUpPriority": { + "name": "followUpPriority", + "type": "FollowUpPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "mediationMinutes": { + "name": "mediationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolutionStage": { + "name": "resolutionStage", + "type": "ResolutionStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'REPORTED'" + }, + "stageEnteredAt": { + "name": "stageEnteredAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Incident_date_idx": { + "name": "Incident_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_nextFollowUpDate_idx": { + "name": "Incident_nextFollowUpDate_idx", + "columns": [ + { + "expression": "nextFollowUpDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_reportedById_idx": { + "name": "Incident_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_subjectId_idx": { + "name": "Incident_subjectId_idx", + "columns": [ + { + "expression": "subjectId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_type_severity_idx": { + "name": "Incident_type_severity_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Incident_housingUnitId_fkey": { + "name": "Incident_housingUnitId_fkey", + "tableFrom": "Incident", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Incident_placementId_fkey": { + "name": "Incident_placementId_fkey", + "tableFrom": "Incident", + "tableTo": "Placement", + "columnsFrom": [ + "placementId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_reportedById_fkey": { + "name": "Incident_reportedById_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": [ + "reportedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_subjectId_fkey": { + "name": "Incident_subjectId_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": [ + "subjectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentFollowUp": { + "name": "IncidentFollowUp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffName": { + "name": "staffName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduledNextDate": { + "name": "scheduledNextDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IncidentFollowUp_createdAt_idx": { + "name": "IncidentFollowUp_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentFollowUp_incidentId_idx": { + "name": "IncidentFollowUp_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentFollowUp_incidentId_fkey": { + "name": "IncidentFollowUp_incidentId_fkey", + "tableFrom": "IncidentFollowUp", + "tableTo": "Incident", + "columnsFrom": [ + "incidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentInvolvement": { + "name": "IncidentInvolvement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "InvolvementRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INVOLVED'" + } + }, + "indexes": { + "IncidentInvolvement_incidentId_residentId_key": { + "name": "IncidentInvolvement_incidentId_residentId_key", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentInvolvement_residentId_idx": { + "name": "IncidentInvolvement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentInvolvement_incidentId_fkey": { + "name": "IncidentInvolvement_incidentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Incident", + "columnsFrom": [ + "incidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "IncidentInvolvement_residentId_fkey": { + "name": "IncidentInvolvement_residentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.LearningRecord": { + "name": "LearningRecord", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "LearningKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "LearningStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "languageCode": { + "name": "languageCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cefrLevel": { + "name": "cefrLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recordedBy": { + "name": "recordedBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LearningRecord_languageCode_cefrLevel_idx": { + "name": "LearningRecord_languageCode_cefrLevel_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cefrLevel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_residentId_kind_idx": { + "name": "LearningRecord_residentId_kind_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_status_idx": { + "name": "LearningRecord_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "LearningRecord_residentId_fkey": { + "name": "LearningRecord_residentId_fkey", + "tableFrom": "LearningRecord", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MaintenanceRequest": { + "name": "MaintenanceRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "MaintenanceCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "MaintenancePriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reporterName": { + "name": "reporterName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedTo": { + "name": "assignedTo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "MaintenanceStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MaintenanceRequest_createdAt_idx": { + "name": "MaintenanceRequest_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_housingUnitId_idx": { + "name": "MaintenanceRequest_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_priority_status_idx": { + "name": "MaintenanceRequest_priority_status_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_reportedById_idx": { + "name": "MaintenanceRequest_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_status_idx": { + "name": "MaintenanceRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MaintenanceRequest_housingUnitId_fkey": { + "name": "MaintenanceRequest_housingUnitId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MaintenanceRequest_spotId_fkey": { + "name": "MaintenanceRequest_spotId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "PlacementSpot", + "columnsFrom": [ + "spotId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "MaintenanceRequest_reportedById_fkey": { + "name": "MaintenanceRequest_reportedById_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "Resident", + "columnsFrom": [ + "reportedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MarketplacePost": { + "name": "MarketplacePost", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postedById": { + "name": "postedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "MarketplacePostKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "status": { + "name": "status", + "type": "MarketplacePostStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "claimedById": { + "name": "claimedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "closedAt": { + "name": "closedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "hiddenByStaff": { + "name": "hiddenByStaff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hiddenReason": { + "name": "hiddenReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactNote": { + "name": "contactNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimedAt": { + "name": "claimedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MarketplacePost_housingUnitId_status_idx": { + "name": "MarketplacePost_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MarketplacePost_postedById_idx": { + "name": "MarketplacePost_postedById_idx", + "columns": [ + { + "expression": "postedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MarketplacePost_housingUnitId_fkey": { + "name": "MarketplacePost_housingUnitId_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_postedById_fkey": { + "name": "MarketplacePost_postedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": [ + "postedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_claimedById_fkey": { + "name": "MarketplacePost_claimedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": [ + "claimedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Message": { + "name": "Message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "threadId": { + "name": "threadId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorResidentId": { + "name": "authorResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorUserId": { + "name": "authorUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "readAt": { + "name": "readAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Message_threadId_createdAt_idx": { + "name": "Message_threadId_createdAt_idx", + "columns": [ + { + "expression": "threadId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Message_threadId_fkey": { + "name": "Message_threadId_fkey", + "tableFrom": "Message", + "tableTo": "MessageThread", + "columnsFrom": [ + "threadId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Message_authorResidentId_fkey": { + "name": "Message_authorResidentId_fkey", + "tableFrom": "Message", + "tableTo": "Resident", + "columnsFrom": [ + "authorResidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Message_authorUserId_fkey": { + "name": "Message_authorUserId_fkey", + "tableFrom": "Message", + "tableTo": "User", + "columnsFrom": [ + "authorUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "Message_one_author": { + "name": "Message_one_author", + "value": "(\"authorResidentId\" IS NOT NULL) <> (\"authorUserId\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.MessageThread": { + "name": "MessageThread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "MessageThread_residentId_key": { + "name": "MessageThread_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MessageThread_updatedAt_idx": { + "name": "MessageThread_updatedAt_idx", + "columns": [ + { + "expression": "updatedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MessageThread_residentId_fkey": { + "name": "MessageThread_residentId_fkey", + "tableFrom": "MessageThread", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Opportunity": { + "name": "Opportunity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "OpportunityKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organisation": { + "name": "organisation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hoursPerWeek": { + "name": "hoursPerWeek", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "germanLevel": { + "name": "germanLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permitRequirement": { + "name": "permitRequirement", + "type": "PermitRequirement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "requirementNote": { + "name": "requirementNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactName": { + "name": "contactName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactEmail": { + "name": "contactEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactPhone": { + "name": "contactPhone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "OpportunityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Opportunity_endsAt_idx": { + "name": "Opportunity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Opportunity_status_kind_idx": { + "name": "Opportunity_status_kind_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Opportunity_createdByUserId_fkey": { + "name": "Opportunity_createdByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": [ + "createdByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Opportunity_updatedByUserId_fkey": { + "name": "Opportunity_updatedByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": [ + "updatedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpportunityApplication": { + "name": "OpportunityApplication", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunityId": { + "name": "opportunityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "ApplicationStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERESTED'" + }, + "stageChangedAt": { + "name": "stageChangedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supportedByUserId": { + "name": "supportedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "learningRecordId": { + "name": "learningRecordId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "OpportunityApplication_learningRecordId_key": { + "name": "OpportunityApplication_learningRecordId_key", + "columns": [ + { + "expression": "learningRecordId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_opportunityId_stage_idx": { + "name": "OpportunityApplication_opportunityId_stage_idx", + "columns": [ + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_idx": { + "name": "OpportunityApplication_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_opportunityId_key": { + "name": "OpportunityApplication_residentId_opportunityId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_stage_idx": { + "name": "OpportunityApplication_stage_idx", + "columns": [ + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "OpportunityApplication_residentId_fkey": { + "name": "OpportunityApplication_residentId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "OpportunityApplication_opportunityId_fkey": { + "name": "OpportunityApplication_opportunityId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Opportunity", + "columnsFrom": [ + "opportunityId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "OpportunityApplication_supportedByUserId_fkey": { + "name": "OpportunityApplication_supportedByUserId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "User", + "columnsFrom": [ + "supportedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "OpportunityApplication_learningRecordId_fkey": { + "name": "OpportunityApplication_learningRecordId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "LearningRecord", + "columnsFrom": [ + "learningRecordId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Placement": { + "name": "Placement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "compatibilityScore": { + "name": "compatibilityScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "PlacementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "endReason": { + "name": "endReason", + "type": "EndReason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "satisfactionRating": { + "name": "satisfactionRating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "placementNotes": { + "name": "placementNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictGap": { + "name": "conflictGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wasPredictable": { + "name": "wasPredictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "relatedIncidentId": { + "name": "relatedIncidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Placement_housingUnitId_idx": { + "name": "Placement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_housingUnitId_startDate_key": { + "name": "Placement_residentId_housingUnitId_startDate_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_idx": { + "name": "Placement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_startDate_endDate_idx": { + "name": "Placement_startDate_endDate_idx", + "columns": [ + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_status_idx": { + "name": "Placement_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Placement_residentId_fkey": { + "name": "Placement_residentId_fkey", + "tableFrom": "Placement", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_housingUnitId_fkey": { + "name": "Placement_housingUnitId_fkey", + "tableFrom": "Placement", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_spotId_fkey": { + "name": "Placement_spotId_fkey", + "tableFrom": "Placement", + "tableTo": "PlacementSpot", + "columnsFrom": [ + "spotId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Placement_relatedIncidentId_fkey": { + "name": "Placement_relatedIncidentId_fkey", + "tableFrom": "Placement", + "tableTo": "Incident", + "columnsFrom": [ + "relatedIncidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PlacementSpot": { + "name": "PlacementSpot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "SpotType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parentSpotId": { + "name": "parentSpotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "squareMeters": { + "name": "squareMeters", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hasPrivateBathroom": { + "name": "hasPrivateBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateKitchen": { + "name": "hasPrivateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateToilet": { + "name": "hasPrivateToilet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requiresMedicalDocs": { + "name": "requiresMedicalDocs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "SpotStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "PlacementSpot_housingUnitId_code_key": { + "name": "PlacementSpot_housingUnitId_code_key", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_housingUnitId_idx": { + "name": "PlacementSpot_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_requiresMedicalDocs_idx": { + "name": "PlacementSpot_requiresMedicalDocs_idx", + "columns": [ + { + "expression": "requiresMedicalDocs", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_type_status_idx": { + "name": "PlacementSpot_type_status_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "PlacementSpot_housingUnitId_fkey": { + "name": "PlacementSpot_housingUnitId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "PlacementSpot_parentSpotId_fkey": { + "name": "PlacementSpot_parentSpotId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "PlacementSpot", + "columnsFrom": [ + "parentSpotId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Proposal": { + "name": "Proposal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ProposalType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "targetRuleId": { + "name": "targetRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parentOrgRuleId": { + "name": "parentOrgRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByResidentId": { + "name": "proposedByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByStaff": { + "name": "proposedByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ProposalStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DISCUSSION'" + }, + "decisionMode": { + "name": "decisionMode", + "type": "DecisionMode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "VoteThreshold", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quorumPercent": { + "name": "quorumPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "approvalPercent": { + "name": "approvalPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "eligibleVoterCount": { + "name": "eligibleVoterCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discussionEndsAt": { + "name": "discussionEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingOpenedAt": { + "name": "votingOpenedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingEndsAt": { + "name": "votingEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "decidedAt": { + "name": "decidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "outcomeSummary": { + "name": "outcomeSummary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecision": { + "name": "staffDecision", + "type": "StaffDecision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffUserId": { + "name": "staffUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecidedAt": { + "name": "staffDecidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Proposal_housingUnitId_status_idx": { + "name": "Proposal_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Proposal_status_votingEndsAt_idx": { + "name": "Proposal_status_votingEndsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "votingEndsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Proposal_housingUnitId_fkey": { + "name": "Proposal_housingUnitId_fkey", + "tableFrom": "Proposal", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_targetRuleId_fkey": { + "name": "Proposal_targetRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": [ + "targetRuleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_parentOrgRuleId_fkey": { + "name": "Proposal_parentOrgRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": [ + "parentOrgRuleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Proposal_proposedByResidentId_fkey": { + "name": "Proposal_proposedByResidentId_fkey", + "tableFrom": "Proposal", + "tableTo": "Resident", + "columnsFrom": [ + "proposedByResidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Resident": { + "name": "Resident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ageRange": { + "name": "ageRange", + "type": "AgeRange", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "Gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "familyStatus": { + "name": "familyStatus", + "type": "FamilyStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sleepSchedule": { + "name": "sleepSchedule", + "type": "SleepSchedule", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "noiseTolerance": { + "name": "noiseTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cleanlinessPractice": { + "name": "cleanlinessPractice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "guestTolerance": { + "name": "guestTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "socialStyle": { + "name": "socialStyle", + "type": "SocialStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "culturalRegion": { + "name": "culturalRegion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictStyle": { + "name": "conflictStyle", + "type": "ConflictStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'COOPERATIVE'" + }, + "smokingStatus": { + "name": "smokingStatus", + "type": "SmokingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dietaryNeeds": { + "name": "dietaryNeeds", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "mobilityNeeds": { + "name": "mobilityNeeds", + "type": "MobilityNeed", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "medicalEquipment": { + "name": "medicalEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petTolerance": { + "name": "petTolerance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedBathroom": { + "name": "sharedBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privacyNeed": { + "name": "privacyNeed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "choresContribution": { + "name": "choresContribution", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "recyclingKnowledge": { + "name": "recyclingKnowledge", + "type": "RecyclingKnowledge", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "roomSharingStatus": { + "name": "roomSharingStatus", + "type": "RoomSharingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CAN_SHARE'" + }, + "hasNightDisturbances": { + "name": "hasNightDisturbances", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needsQuietEnvironment": { + "name": "needsQuietEnvironment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasSleepEquipment": { + "name": "hasSleepEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "supportLevel": { + "name": "supportLevel", + "type": "SupportLevel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'STANDARD'" + }, + "roommatePreferences": { + "name": "roommatePreferences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ResidentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasMedicalDocumentation": { + "name": "hasMedicalDocumentation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "medicalDocType": { + "name": "medicalDocType", + "type": "MedicalDocType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "medicalDocDate": { + "name": "medicalDocDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "medicalDocNotes": { + "name": "medicalDocNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferencesCompletedAt": { + "name": "preferencesCompletedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "cleanlinessExpectation": { + "name": "cleanlinessExpectation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "chaosTolerance": { + "name": "chaosTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profileVisibility": { + "name": "profileVisibility", + "type": "ProfileVisibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ROOMMATES'" + }, + "livingSkillsSupport": { + "name": "livingSkillsSupport", + "type": "LivingSkillsSupport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INDEPENDENT'" + } + }, + "indexes": { + "Resident_ageRange_gender_idx": { + "name": "Resident_ageRange_gender_idx", + "columns": [ + { + "expression": "ageRange", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gender", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_code_key": { + "name": "Resident_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_livingSkillsSupport_idx": { + "name": "Resident_livingSkillsSupport_idx", + "columns": [ + { + "expression": "livingSkillsSupport", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_status_idx": { + "name": "Resident_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocument": { + "name": "ResidentDocument", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizeBytes": { + "name": "sizeBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadedByUserId": { + "name": "uploadedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ResidentDocument_residentId_createdAt_idx": { + "name": "ResidentDocument_residentId_createdAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ResidentDocument_residentId_fkey": { + "name": "ResidentDocument_residentId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ResidentDocument_uploadedByUserId_fkey": { + "name": "ResidentDocument_uploadedByUserId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "User", + "columnsFrom": [ + "uploadedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocumentBlob": { + "name": "ResidentDocumentBlob", + "schema": "", + "columns": { + "documentId": { + "name": "documentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentDocumentBlob_documentId_fkey": { + "name": "ResidentDocumentBlob_documentId_fkey", + "tableFrom": "ResidentDocumentBlob", + "tableTo": "ResidentDocument", + "columnsFrom": [ + "documentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentPhoto": { + "name": "ResidentPhoto", + "schema": "", + "columns": { + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentPhoto_residentId_fkey": { + "name": "ResidentPhoto_residentId_fkey", + "tableFrom": "ResidentPhoto", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RuleAcknowledgement": { + "name": "RuleAcknowledgement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ruleVersion": { + "name": "ruleVersion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "acknowledgedAt": { + "name": "acknowledgedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "RuleAcknowledgement_residentId_idx": { + "name": "RuleAcknowledgement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "RuleAcknowledgement_ruleId_residentId_ruleVersion_key": { + "name": "RuleAcknowledgement_ruleId_residentId_ruleVersion_key", + "columns": [ + { + "expression": "ruleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ruleVersion", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RuleAcknowledgement_ruleId_fkey": { + "name": "RuleAcknowledgement_ruleId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "HouseRule", + "columnsFrom": [ + "ruleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RuleAcknowledgement_residentId_fkey": { + "name": "RuleAcknowledgement_residentId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SatisfactionCheckIn": { + "name": "SatisfactionCheckIn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkInType": { + "name": "checkInType", + "type": "CheckInType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "weekNumber": { + "name": "weekNumber", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "overallSatisfaction": { + "name": "overallSatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roommateRelations": { + "name": "roommateRelations", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "facilitySatisfaction": { + "name": "facilitySatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "safetyFeeling": { + "name": "safetyFeeling", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "improvements": { + "name": "improvements", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "positives": { + "name": "positives", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedBy": { + "name": "collectedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isAnonymous": { + "name": "isAnonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appointmentId": { + "name": "appointmentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedByUserId": { + "name": "collectedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "SatisfactionCheckIn_appointmentId_key": { + "name": "SatisfactionCheckIn_appointmentId_key", + "columns": [ + { + "expression": "appointmentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_checkInType_idx": { + "name": "SatisfactionCheckIn_checkInType_idx", + "columns": [ + { + "expression": "checkInType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_collectedByUserId_idx": { + "name": "SatisfactionCheckIn_collectedByUserId_idx", + "columns": [ + { + "expression": "collectedByUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_placementId_idx": { + "name": "SatisfactionCheckIn_placementId_idx", + "columns": [ + { + "expression": "placementId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "SatisfactionCheckIn_placementId_fkey": { + "name": "SatisfactionCheckIn_placementId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Placement", + "columnsFrom": [ + "placementId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_appointmentId_fkey": { + "name": "SatisfactionCheckIn_appointmentId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Appointment", + "columnsFrom": [ + "appointmentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_collectedByUserId_fkey": { + "name": "SatisfactionCheckIn_collectedByUserId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "User", + "columnsFrom": [ + "collectedByUserId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Settlement": { + "name": "Settlement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromId": { + "name": "fromId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toId": { + "name": "toId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Settlement_housingUnitId_idx": { + "name": "Settlement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Settlement_housingUnitId_fkey": { + "name": "Settlement_housingUnitId_fkey", + "tableFrom": "Settlement", + "tableTo": "HousingUnit", + "columnsFrom": [ + "housingUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Settlement_fromId_fkey": { + "name": "Settlement_fromId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": [ + "fromId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Settlement_toId_fkey": { + "name": "Settlement_toId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": [ + "toId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SystemConfig": { + "name": "SystemConfig", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'singleton'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "pilotBaselineIncidentsPerMonth": { + "name": "pilotBaselineIncidentsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineRelocationsPerMonth": { + "name": "pilotBaselineRelocationsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineMediationHoursPerWeek": { + "name": "pilotBaselineMediationHoursPerWeek", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotStartDate": { + "name": "pilotStartDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskAttentionFlag": { + "name": "TaskAttentionFlag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flaggedById": { + "name": "flaggedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isResolved": { + "name": "isResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolvedByCompletionId": { + "name": "resolvedByCompletionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskAttentionFlag_taskId_idx": { + "name": "TaskAttentionFlag_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskAttentionFlag_taskId_fkey": { + "name": "TaskAttentionFlag_taskId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "HouseholdTask", + "columnsFrom": [ + "taskId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_flaggedById_fkey": { + "name": "TaskAttentionFlag_flaggedById_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "Resident", + "columnsFrom": [ + "flaggedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_resolvedByCompletionId_fkey": { + "name": "TaskAttentionFlag_resolvedByCompletionId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "TaskCompletion", + "columnsFrom": [ + "resolvedByCompletionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskCompletion": { + "name": "TaskCompletion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedById": { + "name": "completedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "durationMinutes": { + "name": "durationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completedItems": { + "name": "completedItems", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "TaskCompletion_completedById_idx": { + "name": "TaskCompletion_completedById_idx", + "columns": [ + { + "expression": "completedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskCompletion_taskId_idx": { + "name": "TaskCompletion_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskCompletion_taskId_fkey": { + "name": "TaskCompletion_taskId_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "HouseholdTask", + "columnsFrom": [ + "taskId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskCompletion_completedById_fkey": { + "name": "TaskCompletion_completedById_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "Resident", + "columnsFrom": [ + "completedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskRequest": { + "name": "TaskRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedById": { + "name": "requestedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedResidentId": { + "name": "requestedResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isBroadcast": { + "name": "isBroadcast", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "TaskRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "responseMessage": { + "name": "responseMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completionId": { + "name": "completionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskRequest_requestedResidentId_idx": { + "name": "TaskRequest_requestedResidentId_idx", + "columns": [ + { + "expression": "requestedResidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskRequest_taskId_idx": { + "name": "TaskRequest_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskRequest_taskId_fkey": { + "name": "TaskRequest_taskId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "HouseholdTask", + "columnsFrom": [ + "taskId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedById_fkey": { + "name": "TaskRequest_requestedById_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": [ + "requestedById" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedResidentId_fkey": { + "name": "TaskRequest_requestedResidentId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": [ + "requestedResidentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TaskRequest_completionId_fkey": { + "name": "TaskRequest_completionId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "TaskCompletion", + "columnsFrom": [ + "completionId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TransferRequest": { + "name": "TransferRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currentPlacementId": { + "name": "currentPlacementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "targetUnitId": { + "name": "targetUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "TransferRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedBy": { + "name": "reviewedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TransferRequest_residentId_idx": { + "name": "TransferRequest_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TransferRequest_status_idx": { + "name": "TransferRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TransferRequest_residentId_fkey": { + "name": "TransferRequest_residentId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TransferRequest_currentPlacementId_fkey": { + "name": "TransferRequest_currentPlacementId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Placement", + "columnsFrom": [ + "currentPlacementId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TransferRequest_targetUnitId_fkey": { + "name": "TransferRequest_targetUnitId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "HousingUnit", + "columnsFrom": [ + "targetUnitId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "StaffRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'BETREUUNG'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "lastLoginAt": { + "name": "lastLoginAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "StaffScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OWN_DOMAIN'" + }, + "isSystemAdmin": { + "name": "isSystemAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "User_code_idx": { + "name": "User_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_role_idx": { + "name": "User_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_scope_idx": { + "name": "User_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_code_key": { + "name": "User_code_key", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Vote": { + "name": "Vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "proposalId": { + "name": "proposalId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "choice": { + "name": "choice", + "type": "VoteChoice", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "castAt": { + "name": "castAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Vote_proposalId_residentId_key": { + "name": "Vote_proposalId_residentId_key", + "columns": [ + { + "expression": "proposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Vote_residentId_idx": { + "name": "Vote_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Vote_proposalId_fkey": { + "name": "Vote_proposalId_fkey", + "tableFrom": "Vote", + "tableTo": "Proposal", + "columnsFrom": [ + "proposalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Vote_residentId_fkey": { + "name": "Vote_residentId_fkey", + "tableFrom": "Vote", + "tableTo": "Resident", + "columnsFrom": [ + "residentId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ActivityCategory": { + "name": "ActivityCategory", + "schema": "public", + "values": [ + "SPORT", + "LANGUAGE", + "CULTURE", + "COMMUNITY", + "FAMILY", + "SUPPORT" + ] + }, + "public.ActivityCost": { + "name": "ActivityCost", + "schema": "public", + "values": [ + "FREE", + "REDUCED", + "PAID" + ] + }, + "public.ActivityStatus": { + "name": "ActivityStatus", + "schema": "public", + "values": [ + "DRAFT", + "PUBLISHED", + "ARCHIVED" + ] + }, + "public.AgeRange": { + "name": "AgeRange", + "schema": "public", + "values": [ + "YOUNG_ADULT", + "ADULT", + "MIDDLE_AGED", + "SENIOR" + ] + }, + "public.AgreementStatus": { + "name": "AgreementStatus", + "schema": "public", + "values": [ + "PROPOSED", + "ACCEPTED", + "HELD", + "BROKEN", + "EXPIRED" + ] + }, + "public.ApplicationStage": { + "name": "ApplicationStage", + "schema": "public", + "values": [ + "INTERESTED", + "APPLIED", + "INTERVIEW", + "ACCEPTED", + "STARTED", + "ENDED", + "DECLINED" + ] + }, + "public.AppointmentStatus": { + "name": "AppointmentStatus", + "schema": "public", + "values": [ + "SCHEDULED", + "COMPLETED", + "CANCELLED", + "NO_SHOW", + "REQUESTED" + ] + }, + "public.AuthTokenPurpose": { + "name": "AuthTokenPurpose", + "schema": "public", + "values": [ + "VERIFY_EMAIL", + "RESET_PASSWORD" + ] + }, + "public.CareRole": { + "name": "CareRole", + "schema": "public", + "values": [ + "HOUSING", + "SOCIAL", + "JOB", + "VOLUNTEERING" + ] + }, + "public.CheckInType": { + "name": "CheckInType", + "schema": "public", + "values": [ + "INITIAL", + "REGULAR", + "AD_HOC", + "EXIT" + ] + }, + "public.ComplaintStatus": { + "name": "ComplaintStatus", + "schema": "public", + "values": [ + "OPEN", + "IN_REVIEW", + "ANSWERED" + ] + }, + "public.ComplaintSubject": { + "name": "ComplaintSubject", + "schema": "public", + "values": [ + "STAFF", + "ACCOMMODATION", + "DECISION", + "OTHER" + ] + }, + "public.ConflictStyle": { + "name": "ConflictStyle", + "schema": "public", + "values": [ + "AVOIDANT", + "COOPERATIVE", + "DIRECT" + ] + }, + "public.DecisionMode": { + "name": "DecisionMode", + "schema": "public", + "values": [ + "RESIDENT_BINDING", + "RESIDENT_ADVISORY", + "STAFF_ONLY" + ] + }, + "public.EndReason": { + "name": "EndReason", + "schema": "public", + "values": [ + "NATURAL", + "CONFLICT", + "REQUEST", + "CAPACITY", + "UPGRADE", + "OTHER" + ] + }, + "public.EventRsvpStatus": { + "name": "EventRsvpStatus", + "schema": "public", + "values": [ + "GOING", + "MAYBE", + "DECLINED" + ] + }, + "public.FamilyStatus": { + "name": "FamilyStatus", + "schema": "public", + "values": [ + "SINGLE", + "COUPLE", + "FAMILY_WITH_CHILDREN", + "SINGLE_PARENT" + ] + }, + "public.FollowUpPriority": { + "name": "FollowUpPriority", + "schema": "public", + "values": [ + "LOW", + "NORMAL", + "HIGH", + "URGENT" + ] + }, + "public.Gender": { + "name": "Gender", + "schema": "public", + "values": [ + "MALE", + "FEMALE", + "OTHER", + "PREFER_NOT_SAY" + ] + }, + "public.HouseEventCategory": { + "name": "HouseEventCategory", + "schema": "public", + "values": [ + "HOUSE_MEETING", + "SOCIAL", + "CULTURE", + "SUPPORT" + ] + }, + "public.HouseEventStatus": { + "name": "HouseEventStatus", + "schema": "public", + "values": [ + "DRAFT", + "PUBLISHED", + "CANCELLED" + ] + }, + "public.HouseholdTaskCategory": { + "name": "HouseholdTaskCategory", + "schema": "public", + "values": [ + "CLEANING", + "SHOPPING", + "MAINTENANCE", + "COOKING", + "TRASH", + "OTHER" + ] + }, + "public.HouseholdTaskPriority": { + "name": "HouseholdTaskPriority", + "schema": "public", + "values": [ + "LOW", + "NORMAL", + "HIGH", + "URGENT" + ] + }, + "public.HouseholdTaskStatus": { + "name": "HouseholdTaskStatus", + "schema": "public", + "values": [ + "IDLE", + "NEEDS_ATTENTION", + "REQUESTED", + "IN_PROGRESS" + ] + }, + "public.HouseholdTaskType": { + "name": "HouseholdTaskType", + "schema": "public", + "values": [ + "ONE_TIME", + "RECURRING_SCHEDULED", + "RECURRING_AS_NEEDED" + ] + }, + "public.HousingStatus": { + "name": "HousingStatus", + "schema": "public", + "values": [ + "AVAILABLE", + "FULL", + "MAINTENANCE", + "CLOSED" + ] + }, + "public.IncidentCategory": { + "name": "IncidentCategory", + "schema": "public", + "values": [ + "INTERPERSONAL", + "MAINTENANCE", + "SAFETY", + "WELLBEING" + ] + }, + "public.IncidentSeverity": { + "name": "IncidentSeverity", + "schema": "public", + "values": [ + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ] + }, + "public.IncidentType": { + "name": "IncidentType", + "schema": "public", + "values": [ + "NOISE_COMPLAINT", + "CLEANLINESS_DISPUTE", + "PERSONAL_CONFLICT", + "CULTURAL_FRICTION", + "SPACE_DISPUTE", + "SCHEDULE_CONFLICT", + "SAFETY_CONCERN", + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY_SYSTEM", + "GENERAL_MAINTENANCE", + "LOW_SATISFACTION", + "OTHER" + ] + }, + "public.InvolvementRole": { + "name": "InvolvementRole", + "schema": "public", + "values": [ + "INVOLVED", + "WITNESS", + "MEDIATOR" + ] + }, + "public.LearningKind": { + "name": "LearningKind", + "schema": "public", + "values": [ + "LANGUAGE_TEST", + "COURSE", + "INFORMAL", + "QUALIFICATION", + "VOLUNTEERING", + "COMMUNITY_SERVICE", + "EMPLOYMENT", + "INTERNSHIP" + ] + }, + "public.LearningStatus": { + "name": "LearningStatus", + "schema": "public", + "values": [ + "PLANNED", + "IN_PROGRESS", + "COMPLETED", + "EXPIRED" + ] + }, + "public.LivingSkillsSupport": { + "name": "LivingSkillsSupport", + "schema": "public", + "values": [ + "INDEPENDENT", + "SOME_SUPPORT", + "REGULAR_SUPPORT" + ] + }, + "public.MaintenanceCategory": { + "name": "MaintenanceCategory", + "schema": "public", + "values": [ + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY", + "CLEANING", + "EXTERIOR", + "OTHER" + ] + }, + "public.MaintenancePriority": { + "name": "MaintenancePriority", + "schema": "public", + "values": [ + "LOW", + "NORMAL", + "HIGH", + "URGENT" + ] + }, + "public.MaintenanceStatus": { + "name": "MaintenanceStatus", + "schema": "public", + "values": [ + "OPEN", + "ASSIGNED", + "IN_PROGRESS", + "ON_HOLD", + "COMPLETED", + "CANCELLED" + ] + }, + "public.MarketplacePostKind": { + "name": "MarketplacePostKind", + "schema": "public", + "values": [ + "GIVE_AWAY", + "LEND", + "WANTED", + "OFFER_HELP", + "NEED_HELP" + ] + }, + "public.MarketplacePostStatus": { + "name": "MarketplacePostStatus", + "schema": "public", + "values": [ + "OPEN", + "CLAIMED", + "CLOSED" + ] + }, + "public.MedicalDocType": { + "name": "MedicalDocType", + "schema": "public", + "values": [ + "PRIVATE_ROOM", + "STUDIO", + "BOTH" + ] + }, + "public.MobilityNeed": { + "name": "MobilityNeed", + "schema": "public", + "values": [ + "NONE", + "GROUND_FLOOR", + "WHEELCHAIR" + ] + }, + "public.OpportunityKind": { + "name": "OpportunityKind", + "schema": "public", + "values": [ + "VOLUNTEERING", + "COMMUNITY_SERVICE", + "EMPLOYMENT", + "INTERNSHIP" + ] + }, + "public.OpportunityStatus": { + "name": "OpportunityStatus", + "schema": "public", + "values": [ + "DRAFT", + "PUBLISHED", + "ARCHIVED" + ] + }, + "public.PermitRequirement": { + "name": "PermitRequirement", + "schema": "public", + "values": [ + "NONE", + "EMPLOYER_NOTIFIES", + "PERMIT_REQUIRED" + ] + }, + "public.PlacementStatus": { + "name": "PlacementStatus", + "schema": "public", + "values": [ + "ACTIVE", + "ENDED", + "TRANSFERRED" + ] + }, + "public.ProfileVisibility": { + "name": "ProfileVisibility", + "schema": "public", + "values": [ + "PRIVATE", + "ROOMMATES", + "RESIDENTS" + ] + }, + "public.ProposalStatus": { + "name": "ProposalStatus", + "schema": "public", + "values": [ + "DISCUSSION", + "VOTING", + "NEEDS_STAFF_CONFIRMATION", + "ACCEPTED", + "REJECTED", + "WITHDRAWN", + "VETOED", + "EXPIRED" + ] + }, + "public.ProposalType": { + "name": "ProposalType", + "schema": "public", + "values": [ + "ADD_RULE", + "AMEND_RULE", + "REPEAL_RULE", + "HOUSE_DECISION" + ] + }, + "public.RecyclingKnowledge": { + "name": "RecyclingKnowledge", + "schema": "public", + "values": [ + "NONE", + "BASIC", + "GOOD" + ] + }, + "public.ResidentOrStaff": { + "name": "ResidentOrStaff", + "schema": "public", + "values": [ + "RESIDENT", + "STAFF" + ] + }, + "public.ResidentStatus": { + "name": "ResidentStatus", + "schema": "public", + "values": [ + "ACTIVE", + "PLACED", + "TRANSFERRED", + "EXITED" + ] + }, + "public.ResolutionStage": { + "name": "ResolutionStage", + "schema": "public", + "values": [ + "REPORTED", + "SELF_RESOLUTION", + "PEER_MEDIATION", + "STAFF_MEDIATION", + "FORMAL_MEASURE", + "CLOSED" + ] + }, + "public.RoomSharingStatus": { + "name": "RoomSharingStatus", + "schema": "public", + "values": [ + "CAN_SHARE", + "PREFERS_PRIVATE", + "NEEDS_PRIVATE" + ] + }, + "public.RuleCategory": { + "name": "RuleCategory", + "schema": "public", + "values": [ + "SAFETY", + "RESPECT", + "NOISE", + "CLEANLINESS", + "KITCHEN", + "BATHROOM", + "GUESTS", + "SHARED_SPACES", + "COSTS", + "COMMUNICATION", + "OTHER" + ] + }, + "public.RuleDelegation": { + "name": "RuleDelegation", + "schema": "public", + "values": [ + "FIXED", + "UNIT_MAY_STRENGTHEN", + "UNIT_DECIDES" + ] + }, + "public.RuleScope": { + "name": "RuleScope", + "schema": "public", + "values": [ + "ORG", + "UNIT" + ] + }, + "public.RuleStatus": { + "name": "RuleStatus", + "schema": "public", + "values": [ + "ACTIVE", + "SUPERSEDED", + "ARCHIVED" + ] + }, + "public.SleepSchedule": { + "name": "SleepSchedule", + "schema": "public", + "values": [ + "EARLY_BIRD", + "STANDARD", + "NIGHT_OWL", + "IRREGULAR" + ] + }, + "public.SmokingStatus": { + "name": "SmokingStatus", + "schema": "public", + "values": [ + "NON_SMOKER", + "OUTDOOR_SMOKER", + "INDOOR_SMOKER" + ] + }, + "public.SocialStyle": { + "name": "SocialStyle", + "schema": "public", + "values": [ + "INTROVERTED", + "MODERATE", + "EXTROVERTED" + ] + }, + "public.SpotStatus": { + "name": "SpotStatus", + "schema": "public", + "values": [ + "AVAILABLE", + "OCCUPIED", + "MAINTENANCE", + "CLOSED" + ] + }, + "public.SpotType": { + "name": "SpotType", + "schema": "public", + "values": [ + "BED", + "PRIVATE_ROOM", + "STUDIO", + "ROOM" + ] + }, + "public.StaffDecision": { + "name": "StaffDecision", + "schema": "public", + "values": [ + "CONFIRMED", + "VETOED" + ] + }, + "public.StaffRole": { + "name": "StaffRole", + "schema": "public", + "values": [ + "ADMIN", + "BETREUUNG", + "SOZIALARBEIT", + "JOBCOACH", + "FREIWILLIGENARBEIT" + ] + }, + "public.StaffScope": { + "name": "StaffScope", + "schema": "public", + "values": [ + "OWN_DOMAIN", + "ALL_DOMAINS" + ] + }, + "public.SupportLevel": { + "name": "SupportLevel", + "schema": "public", + "values": [ + "STANDARD", + "ELEVATED", + "INTENSIVE" + ] + }, + "public.TaskRequestStatus": { + "name": "TaskRequestStatus", + "schema": "public", + "values": [ + "PENDING", + "ACCEPTED", + "DECLINED", + "COMPLETED" + ] + }, + "public.TransferRequestStatus": { + "name": "TransferRequestStatus", + "schema": "public", + "values": [ + "PENDING", + "APPROVED", + "DENIED", + "COMPLETED", + "CANCELLED" + ] + }, + "public.VoteChoice": { + "name": "VoteChoice", + "schema": "public", + "values": [ + "YES", + "NO", + "ABSTAIN", + "BLOCK" + ] + }, + "public.VoteThreshold": { + "name": "VoteThreshold", + "schema": "public", + "values": [ + "CONSENSUS", + "SUPERMAJORITY", + "SIMPLE_MAJORITY" + ] + } + }, + "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 new file mode 100644 index 00000000..fc9b9d9f --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1788303042211, + "tag": "0000_unusual_steel_serpent", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1f53cc02..6e84ef84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", + "@paralleldrive/cuid2": "^3.3.0", "@prisma/client": "^5.17.0", "@sentry/nextjs": "^10.71.0", "@types/bcryptjs": "^2.4.6", @@ -16,11 +17,13 @@ "bcryptjs": "^3.0.3", "bip-kit": "^0.1.0", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "jose": "^6.2.10", "lucide-react": "^1.38.0", "marked": "14.1.4", "next": "^16.3.3", "papaparse": "^5.7.0", + "pg": "^8.23.0", "react": "^19.2.8", "react-dom": "^19.2.8", "tailwindcss": "^4.3.3", @@ -36,8 +39,10 @@ "@types/jest": "^30.0.0", "@types/node": "^26.4.0", "@types/papaparse": "^5.5.2", + "@types/pg": "^8.23.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.5", + "drizzle-kit": "^0.31.10", "eslint": "^10.9.1", "eslint-config-next": "^16.3.3", "jest": "^30.5.0", @@ -798,6 +803,13 @@ "node": ">=18" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -831,1204 +843,2082 @@ "tslib": "^2.4.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=12" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=12" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "20 || >=22" + "node": ">=12" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=12" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=12" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=12" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=12" } }, - "node_modules/@fleet/ai-forms": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/bitbaum/ai-forms.git#68087895ab9d46d73d03b91ebf5e648b969ebd76", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "react": ">=18" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=12" } }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=12" } }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=12" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=12" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ - "arm64" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "node": ">=12" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "node": ">=12" } }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", - "license": "Apache-2.0", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ - "freebsd" + "openbsd" ], - "dependencies": { - "@img/sharp-wasm32": "0.35.4" - }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=12" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "cpu": [ - "arm64" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "sunos" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=12" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ - "x64" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=12" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "cpu": [ - "arm" - ], - "libc": [ - "glibc" + "ia32" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=12" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ - "arm64" - ], - "libc": [ - "glibc" + "x64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=12" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "aix" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" + "arm" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ - "s390x" - ], - "libc": [ - "glibc" + "arm64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ - "arm" - ], - "libc": [ - "glibc" + "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ - "arm64" - ], - "libc": [ - "glibc" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" + "arm" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" + "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ - "s390x" - ], - "libc": [ - "glibc" + "ia32" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ - "x64" - ], - "libc": [ - "glibc" + "loong64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ - "arm64" - ], - "libc": [ - "musl" + "mips64el" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ - "x64" - ], - "libc": [ - "musl" + "ppc64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.3" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ - "wasm32" + "s390x" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.4" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ - "arm64" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ - "ia32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "p-limit": "^2.2.0" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jest/console": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.0.tgz", - "integrity": "sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@jest/types": "30.5.0", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.5.0", - "jest-util": "30.5.0", - "slash": "^3.0.0" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jest/core": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.0.tgz", - "integrity": "sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.5.0", - "@jest/pattern": "30.5.0", - "@jest/reporters": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.5.0", - "jest-config": "30.5.0", - "jest-haste-map": "30.5.0", - "jest-message-util": "30.5.0", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-resolve-dependencies": "30.5.0", - "jest-runner": "30.5.0", - "jest-runtime": "30.5.0", - "jest-snapshot": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", - "jest-watcher": "30.5.0", - "pretty-format": "30.5.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", - "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jest/environment": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz", - "integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@jest/fake-timers": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "jest-mock": "30.5.0" + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", - "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", - "dev": true, + "node_modules/@fleet/ai-forms": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/bitbaum/ai-forms.git#68087895ab9d46d73d03b91ebf5e648b969ebd76", "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/jsdom": "^21.1.7", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, "peerDependencies": { - "canvas": "^3.0.0", - "jsdom": "*" + "react": ">=18" }, "peerDependenciesMeta": { - "canvas": { + "react": { "optional": true } } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18.18.0" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@jest/types": "30.4.1", - "@sinonjs/fake-timers": "^15.4.0", - "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18.18.0" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.4.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18.18.0" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "node": ">=12.22" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": ">=18.18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", - "dev": true, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, + "optional": true, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" - }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-regex-util": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.0.tgz", + "integrity": "sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.5.0", + "jest-util": "30.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.0.tgz", + "integrity": "sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/reporters": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.5.0", + "jest-config": "30.5.0", + "jest-haste-map": "30.5.0", + "jest-message-util": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-resolve-dependencies": "30.5.0", + "jest-runner": "30.5.0", + "jest-runtime": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", + "jest-watcher": "30.5.0", + "pretty-format": "30.5.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz", + "integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.5.0", + "@jest/types": "30.5.0", + "@types/node": "*", + "jest-mock": "30.5.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", + "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-regex-util": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", @@ -2586,6 +3476,18 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2747,6 +3649,20 @@ "node": ">=14" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-3.3.0.tgz", + "integrity": "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1", + "bignumber.js": "^9.3.1", + "error-causes": "^3.0.2" + }, + "bin": { + "cuid2": "bin/cuid2.js" + } + }, "node_modules/@parcel/watcher": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", @@ -4781,6 +5697,18 @@ "@types/node": "*" } }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", @@ -6230,6 +7158,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bip-kit": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bip-kit/-/bip-kit-0.1.0.tgz", @@ -6323,8 +7260,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.9", @@ -6890,6 +7826,147 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6964,6 +8041,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-causes": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/error-causes/-/error-causes-3.0.2.tgz", + "integrity": "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==", + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -7153,8 +8236,50 @@ "engines": { "node": ">= 0.4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -8143,180 +9268,454 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", - "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "hermes-estree": "0.25.1" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=10.13.0" + "node": ">= 14" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">= 14" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": "20 || >=22" + "node": ">= 6" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "license": "BlueOak-1.0.0", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.8" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=0.10.0" } }, - "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=18" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.8.19" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "node": ">= 0.4" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { "node": ">= 0.4" }, @@ -8324,36 +9723,41 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.0" + "has-bigints": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8362,12 +9766,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, "engines": { "node": ">= 0.4" }, @@ -8375,15 +9783,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" - }, + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8391,205 +9806,181 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^3.1.1" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=0.10.0" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", - "debug": "4" + "call-bound": "^1.0.3" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=10.17.0" + "node": ">=8" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-in-the-middle": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", - "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", - "license": "Apache-2.0", "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.19" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.12.0" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8598,25 +9989,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", + "call-bound": "^1.0.2", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -8625,15 +10024,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, "engines": { "node": ">= 0.4" }, @@ -8641,15 +10037,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -8658,37 +10053,28 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.3" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8697,16 +10083,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8715,15 +10101,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8732,20 +10117,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-finalizationregistry": { + "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -8758,2973 +10146,3331 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=10" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "is-extglob": "^2.1.1" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "node_modules/jest": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.0.tgz", + "integrity": "sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "@jest/core": "30.5.0", + "@jest/types": "30.5.0", + "import-local": "^3.2.0", + "jest-cli": "30.5.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "node_modules/jest-changed-files": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.0.tgz", + "integrity": "sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==", "dev": true, - "license": "MIT" - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "license": "MIT", "dependencies": { - "@types/estree": "*" + "execa": "^5.1.1", + "jest-util": "30.5.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/jest-circus": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.0.tgz", + "integrity": "sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "@jest/environment": "30.5.0", + "@jest/expect": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.5.0", + "jest-matcher-utils": "30.5.0", + "jest-message-util": "30.5.0", + "jest-runtime": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", + "p-limit": "^3.1.0", + "pretty-format": "30.5.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/jest-cli": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.0.tgz", + "integrity": "sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "node_modules/jest-config": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.0.tgz", + "integrity": "sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "@babel/core": "^7.27.4", + "@jest/get-type": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/test-sequencer": "30.5.0", + "@jest/types": "30.5.0", + "babel-jest": "30.5.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "jest-circus": "30.5.0", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-runner": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", + "parse-json": "^5.2.0", + "pretty-format": "30.5.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-diff": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.0.tgz", + "integrity": "sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", + "chalk": "^4.1.2", + "pretty-format": "30.5.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "node_modules/jest-docblock": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", + "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "detect-newline": "^3.1.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "node_modules/jest-each": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.0.tgz", + "integrity": "sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.0", + "chalk": "^4.1.2", + "jest-util": "30.5.0", + "pretty-format": "30.5.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/jest-environment-jsdom": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", + "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.16" + "@jest/environment": "30.4.1", + "@jest/environment-jsdom-abstract": "30.4.1", + "jsdom": "^26.1.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/jest-environment-jsdom/node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@types/node": "*", + "jest-regex-util": "30.4.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">=10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/jest-environment-jsdom/node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, "engines": { - "node": ">= 0.4" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.0.tgz", - "integrity": "sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==", + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/core": "30.5.0", - "@jest/types": "30.5.0", - "import-local": "^3.2.0", - "jest-cli": "30.5.0" - }, - "bin": { - "jest": "bin/jest.js" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "node": ">=12" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/jest-changed-files": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.0.tgz", - "integrity": "sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==", + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.5.0", - "p-limit": "^3.1.0" + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-circus": { + "node_modules/jest-environment-node": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.0.tgz", - "integrity": "sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.0.tgz", + "integrity": "sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==", "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "30.5.0", - "@jest/expect": "30.5.0", - "@jest/test-result": "30.5.0", + "@jest/fake-timers": "30.5.0", "@jest/types": "30.5.0", "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.5.0", - "jest-matcher-utils": "30.5.0", - "jest-message-util": "30.5.0", - "jest-runtime": "30.5.0", - "jest-snapshot": "30.5.0", + "jest-mock": "30.5.0", "jest-util": "30.5.0", - "p-limit": "^3.1.0", - "pretty-format": "30.5.0", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "jest-validate": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-cli": { + "node_modules/jest-haste-map": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.0.tgz", - "integrity": "sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.0.tgz", + "integrity": "sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.5.0", - "@jest/test-result": "30.5.0", "@jest/types": "30.5.0", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.5.0", + "@parcel/watcher": "^2.6.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "fdir": "^6.5.0", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.5.0", "jest-util": "30.5.0", - "jest-validate": "30.5.0", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" + "jest-worker": "30.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "node-notifier": { + "picomatch": { "optional": true } } }, - "node_modules/jest-config": { + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-leak-detector": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.0.tgz", - "integrity": "sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.0.tgz", + "integrity": "sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", "@jest/get-type": "30.5.0", - "@jest/pattern": "30.5.0", - "@jest/test-sequencer": "30.5.0", - "@jest/types": "30.5.0", - "babel-jest": "30.5.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "jest-circus": "30.5.0", - "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.0", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-runner": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", - "parse-json": "^5.2.0", - "pretty-format": "30.5.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } } }, - "node_modules/jest-diff": { + "node_modules/jest-matcher-utils": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.0.tgz", - "integrity": "sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.0.tgz", + "integrity": "sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.5.0", "@jest/get-type": "30.5.0", "chalk": "^4.1.2", + "jest-diff": "30.5.0", "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-docblock": { + "node_modules/jest-message-util": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", - "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz", + "integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==", "dev": true, "license": "MIT", "dependencies": { - "detect-newline": "^3.1.0" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.5.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.5.0", + "picomatch": "^4.0.3", + "pretty-format": "30.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each": { + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-mock": { "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.0.tgz", - "integrity": "sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.0.tgz", + "integrity": "sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.5.0", + "@jest/expect-utils": "30.5.0", "@jest/types": "30.5.0", - "chalk": "^4.1.2", - "jest-util": "30.5.0", - "pretty-format": "30.5.0" + "@types/node": "*", + "jest-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", - "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", + "node_modules/jest-regex-util": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/environment-jsdom-abstract": "30.4.1", - "jsdom": "^26.1.0" - }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "node_modules/jest-resolve": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.0.tgz", + "integrity": "sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.12.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.0.tgz", + "integrity": "sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1" + "jest-regex-util": "30.5.0", + "jest-snapshot": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "node_modules/jest-runner": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.0.tgz", + "integrity": "sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", - "@sinonjs/fake-timers": "^15.4.0", + "@jest/console": "30.5.0", + "@jest/environment": "30.5.0", + "@jest/source-map": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.0", + "jest-haste-map": "30.5.0", + "jest-leak-detector": "30.5.0", + "jest-message-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-runtime": "30.5.0", + "jest-util": "30.5.0", + "jest-watcher": "30.5.0", + "jest-worker": "30.5.0", + "p-limit": "^3.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "node_modules/jest-runtime": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.0.tgz", + "integrity": "sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==", "dev": true, "license": "MIT", "dependencies": { + "@jest/environment": "30.5.0", + "@jest/fake-timers": "30.5.0", + "@jest/globals": "30.5.0", + "@jest/source-map": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-regex-util": "30.4.0" + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.2.0", + "collect-v8-coverage": "^1.0.2", + "es-module-lexer": "^2.1.0", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.5.0", + "jest-message-util": "30.5.0", + "jest-mock": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "node_modules/jest-snapshot": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.0.tgz", + "integrity": "sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.5.0", + "@jest/get-type": "30.5.0", + "@jest/snapshot-utils": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.5.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.5.0", + "jest-matcher-utils": "30.5.0", + "jest-message-util": "30.5.0", + "jest-util": "30.5.0", + "pretty-format": "30.5.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "node_modules/jest-util": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.0.tgz", + "integrity": "sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", + "@jest/types": "30.5.0", "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "node_modules/jest-validate": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.0.tgz", + "integrity": "sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.0", + "camelcase": "^6.3.0", "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "leven": "^3.1.0", + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-environment-jsdom/node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "node_modules/jest-watcher": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.0.tgz", + "integrity": "sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.5.0", + "string-length": "^4.0.2" + }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "node_modules/jest-worker": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.0.tgz", + "integrity": "sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.5.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jest-environment-jsdom/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" } }, - "node_modules/jest-environment-node": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.0.tgz", - "integrity": "sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/fake-timers": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "jest-mock": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jest-haste-map": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.0.tgz", - "integrity": "sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==", + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", - "@parcel/watcher": "^2.6.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "fdir": "^6.5.0", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.5.0", - "jest-util": "30.5.0", - "jest-worker": "30.5.0", - "picomatch": "^4.0.3" + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" + "node": ">=18" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { - "picomatch": { + "canvas": { "optional": true } } }, - "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/jsdom/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">= 14" } }, - "node_modules/jest-leak-detector": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.0.tgz", - "integrity": "sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==", + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.5.0", - "pretty-format": "30.5.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 14" } }, - "node_modules/jest-matcher-utils": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.0.tgz", - "integrity": "sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==", + "node_modules/jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.5.0", - "chalk": "^4.1.2", - "jest-diff": "30.5.0", - "pretty-format": "30.5.0" + "punycode": "^2.3.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/jest-message-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz", - "integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==", + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.5.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.5.0", - "picomatch": "^4.0.3", - "pretty-format": "30.5.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/jest-message-util/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=12" + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=6" } }, - "node_modules/jest-mock": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.0.tgz", - "integrity": "sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "jest-util": "30.5.0" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=4.0" } }, - "node_modules/jest-regex-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", - "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/jest-resolve": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.0.tgz", - "integrity": "sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==", + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", - "slash": "^3.0.0", - "unrs-resolver": "^1.12.1" + "language-subtag-registry": "^0.3.20" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10" } }, - "node_modules/jest-resolve-dependencies": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.0.tgz", - "integrity": "sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", - "dependencies": { - "jest-regex-util": "30.5.0", - "jest-snapshot": "30.5.0" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=6" } }, - "node_modules/jest-runner": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.0.tgz", - "integrity": "sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.5.0", - "@jest/environment": "30.5.0", - "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.0", - "jest-haste-map": "30.5.0", - "jest-leak-detector": "30.5.0", - "jest-message-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-runtime": "30.5.0", - "jest-util": "30.5.0", - "jest-watcher": "30.5.0", - "jest-worker": "30.5.0", - "p-limit": "^3.1.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.8.0" } }, - "node_modules/jest-runtime": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.0.tgz", - "integrity": "sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/fake-timers": "30.5.0", - "@jest/globals": "30.5.0", - "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.2.0", - "collect-v8-coverage": "^1.0.2", - "es-module-lexer": "^2.1.0", - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", - "jest-message-util": "30.5.0", - "jest-mock": "30.5.0", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-snapshot": "30.5.0", - "jest-util": "30.5.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "detect-libc": "^2.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/jest-snapshot": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.0.tgz", - "integrity": "sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==", + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.5.0", - "@jest/get-type": "30.5.0", - "@jest/snapshot-utils": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.5.0", - "graceful-fs": "^4.2.11", - "jest-diff": "30.5.0", - "jest-matcher-utils": "30.5.0", - "jest-message-util": "30.5.0", - "jest-util": "30.5.0", - "pretty-format": "30.5.0", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.0.tgz", - "integrity": "sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-validate": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.0.tgz", - "integrity": "sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0", - "@jest/types": "30.5.0", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.5.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-watcher": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.0.tgz", - "integrity": "sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.5.0", - "@jest/types": "30.5.0", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.5.0", - "string-length": "^4.0.2" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.0.tgz", - "integrity": "sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.5.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" + "node": ">= 12.0.0" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jose": { - "version": "6.2.10", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", - "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", - "license": "MIT", + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, "funding": { - "url": "https://github.com/sponsors/panva" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", - "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" + "node": ">= 12.0.0" }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 14" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, + "peer": true, "engines": { - "node": ">= 14" + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/jsdom/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "license": "MIT", "dependencies": { - "punycode": "^2.3.1" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=18" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" + "node_modules/lucide-react": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.38.0.tgz", + "integrity": "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "dependencies": { + "semver": "^7.5.3" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, + "license": "ISC" + }, + "node_modules/marked": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", + "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" + "bin": { + "marked": "bin/marked.js" }, "engines": { - "node": ">=4.0" + "node": ">= 18" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, "engines": { - "node": ">=0.10" + "node": ">= 8" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", + "node_modules/meriyah": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", + "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", + "license": "ISC", "engines": { - "node": ">=6" + "node": ">=18.0.0" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8.6" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=4" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": "*" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, + "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, "engines": { - "node": ">= 12.0.0" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/next": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.3.tgz", + "integrity": "sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.3", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=20.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.3", + "@next/swc-darwin-x64": "16.3.3", + "@next/swc-linux-arm64-gnu": "16.3.3", + "@next/swc-linux-arm64-musl": "16.3.3", + "@next/swc-linux-x64-gnu": "16.3.3", + "@next/swc-linux-x64-musl": "16.3.3", + "@next/swc-win32-arm64-msvc": "16.3.3", + "@next/swc-win32-x64-msvc": "16.3.3", + "sharp": "^0.35.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "node_modules/next/node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } + "license": "MIT" }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "license": "MIT", - "peer": true, "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "path-key": "^3.0.0" }, - "bin": { - "loose-envify": "cli.js" + "engines": { + "node": ">=8" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" }, - "node_modules/lucide-react": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.38.0.tgz", - "integrity": "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">= 0.4" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, - "license": "ISC" - }, - "node_modules/marked": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", - "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", "license": "MIT", - "bin": { - "marked": "bin/marked.js" + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, "engines": { - "node": ">= 8" - } - }, - "node_modules/meriyah": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz", - "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==", - "license": "ISC", - "engines": { - "node": ">=18.0.0" + "node": ">= 0.4" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": ">=6" + "node": ">= 0.8.0" } }, - "node_modules/min-indent": { + "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=6" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/papaparse": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz", + "integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==", "license": "MIT" }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "entities": "^6.0.0" }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8" } }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" + "node": ">=8" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/next": { - "version": "16.3.3", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.3.tgz", - "integrity": "sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", "license": "MIT", "dependencies": { - "@next/env": "16.3.3", - "@swc/helpers": "0.5.23", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.5.23", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { - "node": ">=20.9.0" + "node": ">= 16.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.3", - "@next/swc-darwin-x64": "16.3.3", - "@next/swc-linux-arm64-gnu": "16.3.3", - "@next/swc-linux-arm64-musl": "16.3.3", - "@next/swc-linux-x64-gnu": "16.3.3", - "@next/swc-linux-x64-musl": "16.3.3", - "@next/swc-win32-arm64-msvc": "16.3.3", - "@next/swc-win32-x64-msvc": "16.3.3", - "sharp": "^0.35.3" + "pg-cloudflare": "^1.4.0" }, "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" + "pg-native": ">=3.0.1" }, "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { + "pg-native": { "optional": true } } }, - "node_modules/next/node_modules/postcss": { - "version": "8.5.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", - "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=4.0.0" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", "license": "MIT" }, - "node_modules/node-exports-info": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", - "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", - "dev": true, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "engines": { + "node": ">= 6" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "find-up": "^4.0.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=8" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.0.0" + "p-limit": "^2.2.0" }, "engines": { "node": ">=8" } }, - "node_modules/nwsapi": { - "version": "2.2.24", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", - "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", - "dev": true, - "license": "MIT" + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=20" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || >=14" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=4" } }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" + "xtend": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/object.values": { + "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8.0" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=6" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/pretty-format": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" }, "engines": { - "node": ">= 0.4" + "node": ">=16.13" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "2.3.3" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/papaparse": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.7.0.tgz", - "integrity": "sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT" }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "scheduler": "^0.27.0" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "peerDependencies": { + "react": "^19.2.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": ">=0.10.0" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, "engines": { - "node": ">= 6" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^4.0.0" + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "resolve-from": "^5.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/pkg-dir/node_modules/locate-path": { + "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { "node": ">=8" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, "engines": { - "node": ">=8" + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "license": "MIT", "dependencies": { - "playwright-core": "1.62.1" + "@types/estree": "1.0.8" }, "bin": { - "playwright": "cli.js" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=20" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "fsevents": "2.3.2" + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" } }, - "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" }, "engines": { - "node": ">=20" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/possible-typed-array-names": { + "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "xmlchars": "^2.2.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=v12.22.7" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, - "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", - "dev": true, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=14" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", - "dev": true, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { - "@jest/react-is-18": "npm:react-is@^18.3.1", - "@jest/react-is-19": "npm:react-is@^19.2.5", - "@jest/schemas": "30.5.0", - "ansi-styles": "^5.2.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", - "engines": { - "node": ">=10" + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/semifies": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", + "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", + "license": "Apache-2.0" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/engines": "5.22.0" - }, + "license": "ISC", "bin": { - "prisma": "build/index.js" + "semver": "bin/semver.js" }, "engines": { - "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" + "node": ">=10" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.4" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" } }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "shebang-regex": "^3.0.0" }, - "peerDependencies": { - "react": "^19.2.8" + "engines": { + "node": ">=8" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/redent": { + "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11733,19 +13479,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -11754,52 +13496,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-in-the-middle": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", - "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", - "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=9.3.0 || >=8.10.0 <9.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -11808,1432 +13535,1469 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { - "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true, "license": "MIT" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" + "type-fest": "^0.7.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/safe-regex-test": { + "node_modules/stacktrace-parser/node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.2.1" + "internal-slot": "^1.1.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "xmlchars": "^2.2.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=v12.22.7" + "node": ">=8" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=8" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 0.4" } }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" }, - "peerDependencies": { - "ajv": "^8.8.2" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { + "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/semifies": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", - "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==", - "license": "Apache-2.0" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "devOptional": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", - "license": "Apache-2.0", - "optional": true, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", "dependencies": { - "@img/colour": "^1.1.0", - "detect-libc": "^2.1.2", - "semver": "^7.8.5" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=8" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "min-indent": "^1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" + "client-only": "0.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "escape-string-regexp": "^2.0.0" + "@pkgr/core": "^0.3.6" }, "engines": { - "node": ">=10" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" } }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "license": "MIT", + "node_modules/terser": { + "version": "5.48.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", + "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", + "license": "BSD-2-Clause", + "peer": true, "dependencies": { - "type-fest": "^0.7.1" + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" }, "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "license": "(MIT OR CC0-1.0)", + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, "engines": { - "node": ">=8" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 10.13.0" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT", + "peer": true + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/string.prototype.matchall": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", - "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "MIT", + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.2", - "get-intrinsic": "^1.3.0", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.4", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">= 0.4" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "tldts-core": "^6.1.86" }, - "engines": { - "node": ">=8" + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=8" + "node": ">=8.0" } }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", "dev": true, "license": "MIT", "dependencies": { - "min-indent": "^1.0.0" + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=8" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", "dependencies": { - "client-only": "0.0.1" + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" }, - "engines": { - "node": ">= 12.0.0" + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" }, "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" }, "peerDependenciesMeta": { - "@babel/core": { + "@swc/core": { "optional": true }, - "babel-plugin-macros": { + "@swc/wasm": { "optional": true } } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/synckit": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", - "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.3.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tailwindcss": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", - "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", - "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" + "esbuild": "~0.28.0" }, "bin": { - "terser": "bin/terser" + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=10" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", - "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@minify-html/node": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "@swc/html": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "cssnano": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "html-minifier-terser": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "postcss": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=18" } }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 10.13.0" + "node": ">=18" } }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=18" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "peer": true + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=18" } }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "20 || >=22" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "ISC" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=18" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=18" } }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">=18" } }, - "node_modules/ts-jest": { - "version": "29.4.12", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", - "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.9", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.8.5", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } + "node": ">=18" } }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/ts-node/node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -13897,6 +15661,15 @@ "dev": true, "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index d3e24bfb..db018a1e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", + "@paralleldrive/cuid2": "^3.3.0", "@prisma/client": "^5.17.0", "@sentry/nextjs": "^10.71.0", "@types/bcryptjs": "^2.4.6", @@ -37,11 +38,13 @@ "bcryptjs": "^3.0.3", "bip-kit": "^0.1.0", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "jose": "^6.2.10", "lucide-react": "^1.38.0", "marked": "14.1.4", "next": "^16.3.3", "papaparse": "^5.7.0", + "pg": "^8.23.0", "react": "^19.2.8", "react-dom": "^19.2.8", "tailwindcss": "^4.3.3", @@ -57,8 +60,10 @@ "@types/jest": "^30.0.0", "@types/node": "^26.4.0", "@types/papaparse": "^5.5.2", + "@types/pg": "^8.23.1", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.5", + "drizzle-kit": "^0.31.10", "eslint": "^10.9.1", "eslint-config-next": "^16.3.3", "jest": "^30.5.0", diff --git a/src/lib/db.ts b/src/lib/db.ts deleted file mode 100644 index 3b0575d0..00000000 --- a/src/lib/db.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PrismaClient } from '@prisma/client' - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined -} - -function makePrismaClient() { - // Plain TCP to PostgreSQL. Live: Hetzner box bitbaum, database aoz_wohnen, - // loopback-only on the box. See docs/INFRASTRUCTURE.md. Not a cloud pooler. - return new PrismaClient() -} - -export const prisma = globalForPrisma.prisma ?? makePrismaClient() - -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts new file mode 100644 index 00000000..e4466df2 --- /dev/null +++ b/src/lib/db/index.ts @@ -0,0 +1,32 @@ +import { Pool } from 'pg' +import { drizzle } from 'drizzle-orm/node-postgres' +import * as schemaTables from './schema' +import * as schemaRelations from './relations' + +// db.query needs tables AND relations in one schema object. +const schema = { ...schemaTables, ...schemaRelations } + +// Lazy singleton (fleet pattern, see reparaturbonus-zh / vitareba): the Pool +// is not created at module load time, so Next.js build-time page analysis +// doesn't throw when DATABASE_URL is absent in the build environment. +type DbInstance = ReturnType> + +const globalForDb = globalThis as unknown as { db: DbInstance | undefined } + +function getInstance(): DbInstance { + if (!globalForDb.db) { + const url = process.env.DATABASE_URL + if (!url) throw new Error('DATABASE_URL environment variable is not set') + globalForDb.db = drizzle(new Pool({ connectionString: url }), { schema }) + } + return globalForDb.db +} + +export const db = new Proxy({} as DbInstance, { + get(_, prop: string | symbol) { + return Reflect.get(getInstance(), prop) + }, +}) + +export * from './schema' +export * from './types' diff --git a/src/lib/db/relations.ts b/src/lib/db/relations.ts new file mode 100644 index 00000000..e1bdd549 --- /dev/null +++ b/src/lib/db/relations.ts @@ -0,0 +1,697 @@ +/** + * Drizzle relations — field names mirror the old Prisma schema's relation + * fields exactly (`photo`, `messagesWritten`, `expensesPaid`, …) so every + * former `include:` reads the same as its `with:` replacement. Where Prisma + * named a relation (`@relation("ExpensePayer")`) the same string is used as + * drizzle's `relationName`, pinned on both sides. + */ +import { relations } from 'drizzle-orm/relations' +import { + account, + activity, + agreementParty, + appointment, + auditLog, + authToken, + careAssignment, + careAttribute, + compatibilityAssessment, + complaint, + conflictAgreement, + eventRsvp, + expense, + expenseShare, + houseEvent, + householdTask, + houseRule, + housingUnit, + incident, + incidentFollowUp, + incidentInvolvement, + learningRecord, + maintenanceRequest, + marketplacePost, + message, + messageThread, + opportunity, + opportunityApplication, + placement, + placementSpot, + proposal, + resident, + residentDocument, + residentDocumentBlob, + residentPhoto, + ruleAcknowledgement, + satisfactionCheckIn, + settlement, + taskAttentionFlag, + taskCompletion, + taskRequest, + transferRequest, + user, + vote, +} from './schema' + +export const residentRelations = relations(resident, ({ one, many }) => ({ + photo: one(residentPhoto), + documents: many(residentDocument), + complaints: many(complaint), + messageThread: one(messageThread), + messagesWritten: many(message, { relationName: 'MessageAuthor' }), + account: one(account), + placements: many(placement), + assessments: many(compatibilityAssessment, { relationName: 'ResidentAssessments' }), + comparedWith: many(compatibilityAssessment, { relationName: 'ComparedResidentAssessments' }), + incidentsReported: many(incident, { relationName: 'IncidentReporter' }), + incidentsAsSubject: many(incident, { relationName: 'IncidentSubject' }), + incidentInvolvements: many(incidentInvolvement), + maintenanceRequests: many(maintenanceRequest), + createdTasks: many(householdTask, { relationName: 'TaskCreator' }), + taskCompletions: many(taskCompletion), + taskAttentionFlags: many(taskAttentionFlag), + taskRequestsMade: many(taskRequest, { relationName: 'TaskRequestsMade' }), + taskRequestsReceived: many(taskRequest, { relationName: 'TaskRequestsReceived' }), + transferRequests: many(transferRequest), + ruleAcknowledgements: many(ruleAcknowledgement), + proposalsMade: many(proposal, { relationName: 'ProposalAuthor' }), + votes: many(vote), + agreementParties: many(agreementParty), + expensesPaid: many(expense, { relationName: 'ExpensePayer' }), + expensesCreated: many(expense, { relationName: 'ExpenseCreator' }), + expenseShares: many(expenseShare), + settlementsPaid: many(settlement, { relationName: 'SettlementFrom' }), + settlementsRecvd: many(settlement, { relationName: 'SettlementTo' }), + learningRecords: many(learningRecord), + careAssignments: many(careAssignment), + appointments: many(appointment), + careAttributes: many(careAttribute), + opportunityApplications: many(opportunityApplication), + marketplacePostsCreated: many(marketplacePost, { relationName: 'MarketplacePostedBy' }), + marketplacePostsClaimed: many(marketplacePost, { relationName: 'MarketplacePostClaimedBy' }), + houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByResident' }), + eventRsvps: many(eventRsvp), +})) + +export const residentPhotoRelations = relations(residentPhoto, ({ one }) => ({ + resident: one(resident, { + fields: [residentPhoto.residentId], + references: [resident.id], + }), +})) + +export const residentDocumentRelations = relations(residentDocument, ({ one }) => ({ + resident: one(resident, { + fields: [residentDocument.residentId], + references: [resident.id], + }), + uploadedBy: one(user, { + fields: [residentDocument.uploadedByUserId], + references: [user.id], + relationName: 'DocumentUploadedBy', + }), + blob: one(residentDocumentBlob), +})) + +export const residentDocumentBlobRelations = relations(residentDocumentBlob, ({ one }) => ({ + document: one(residentDocument, { + fields: [residentDocumentBlob.documentId], + references: [residentDocument.id], + }), +})) + +export const complaintRelations = relations(complaint, ({ one }) => ({ + resident: one(resident, { + fields: [complaint.residentId], + references: [resident.id], + }), + respondedBy: one(user, { + fields: [complaint.respondedByUserId], + references: [user.id], + relationName: 'ComplaintRespondedBy', + }), +})) + +export const messageThreadRelations = relations(messageThread, ({ one, many }) => ({ + resident: one(resident, { + fields: [messageThread.residentId], + references: [resident.id], + }), + messages: many(message), +})) + +export const messageRelations = relations(message, ({ one }) => ({ + thread: one(messageThread, { + fields: [message.threadId], + references: [messageThread.id], + }), + authorResident: one(resident, { + fields: [message.authorResidentId], + references: [resident.id], + relationName: 'MessageAuthor', + }), + authorUser: one(user, { + fields: [message.authorUserId], + references: [user.id], + relationName: 'MessageAuthor', + }), +})) + +export const housingUnitRelations = relations(housingUnit, ({ many }) => ({ + spots: many(placementSpot), + placements: many(placement), + incidents: many(incident), + maintenanceRequests: many(maintenanceRequest), + householdTasks: many(householdTask), + transferRequests: many(transferRequest), + marketplacePosts: many(marketplacePost), + houseEvents: many(houseEvent), + houseRules: many(houseRule), + proposals: many(proposal), + expenses: many(expense), + settlements: many(settlement), +})) + +export const expenseRelations = relations(expense, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [expense.housingUnitId], + references: [housingUnit.id], + }), + paidBy: one(resident, { + fields: [expense.paidById], + references: [resident.id], + relationName: 'ExpensePayer', + }), + createdBy: one(resident, { + fields: [expense.createdById], + references: [resident.id], + relationName: 'ExpenseCreator', + }), + shares: many(expenseShare), +})) + +export const expenseShareRelations = relations(expenseShare, ({ one }) => ({ + expense: one(expense, { + fields: [expenseShare.expenseId], + references: [expense.id], + }), + resident: one(resident, { + fields: [expenseShare.residentId], + references: [resident.id], + }), +})) + +export const settlementRelations = relations(settlement, ({ one }) => ({ + housingUnit: one(housingUnit, { + fields: [settlement.housingUnitId], + references: [housingUnit.id], + }), + from: one(resident, { + fields: [settlement.fromId], + references: [resident.id], + relationName: 'SettlementFrom', + }), + to: one(resident, { + fields: [settlement.toId], + references: [resident.id], + relationName: 'SettlementTo', + }), +})) + +export const placementSpotRelations = relations(placementSpot, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [placementSpot.housingUnitId], + references: [housingUnit.id], + }), + parentSpot: one(placementSpot, { + fields: [placementSpot.parentSpotId], + references: [placementSpot.id], + relationName: 'SpotHierarchy', + }), + childSpots: many(placementSpot, { relationName: 'SpotHierarchy' }), + placements: many(placement), + maintenanceRequests: many(maintenanceRequest), +})) + +export const placementRelations = relations(placement, ({ one, many }) => ({ + resident: one(resident, { + fields: [placement.residentId], + references: [resident.id], + }), + housingUnit: one(housingUnit, { + fields: [placement.housingUnitId], + references: [housingUnit.id], + }), + spot: one(placementSpot, { + fields: [placement.spotId], + references: [placementSpot.id], + }), + relatedIncident: one(incident, { + fields: [placement.relatedIncidentId], + references: [incident.id], + relationName: 'PlacementConflictIncident', + }), + incidents: many(incident, { relationName: 'IncidentPlacement' }), + checkIns: many(satisfactionCheckIn), + transferRequests: many(transferRequest, { relationName: 'TransferFromPlacement' }), +})) + +export const compatibilityAssessmentRelations = relations(compatibilityAssessment, ({ one }) => ({ + resident: one(resident, { + fields: [compatibilityAssessment.residentId], + references: [resident.id], + relationName: 'ResidentAssessments', + }), + comparedWith: one(resident, { + fields: [compatibilityAssessment.comparedWithId], + references: [resident.id], + relationName: 'ComparedResidentAssessments', + }), +})) + +export const incidentRelations = relations(incident, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [incident.housingUnitId], + references: [housingUnit.id], + }), + placement: one(placement, { + fields: [incident.placementId], + references: [placement.id], + relationName: 'IncidentPlacement', + }), + reportedBy: one(resident, { + fields: [incident.reportedById], + references: [resident.id], + relationName: 'IncidentReporter', + }), + subject: one(resident, { + fields: [incident.subjectId], + references: [resident.id], + relationName: 'IncidentSubject', + }), + involvedResidents: many(incidentInvolvement), + followUps: many(incidentFollowUp), + agreements: many(conflictAgreement), + conflictPlacements: many(placement, { relationName: 'PlacementConflictIncident' }), +})) + +export const incidentFollowUpRelations = relations(incidentFollowUp, ({ one }) => ({ + incident: one(incident, { + fields: [incidentFollowUp.incidentId], + references: [incident.id], + }), +})) + +export const incidentInvolvementRelations = relations(incidentInvolvement, ({ one }) => ({ + incident: one(incident, { + fields: [incidentInvolvement.incidentId], + references: [incident.id], + }), + resident: one(resident, { + fields: [incidentInvolvement.residentId], + references: [resident.id], + }), +})) + +export const satisfactionCheckInRelations = relations(satisfactionCheckIn, ({ one }) => ({ + placement: one(placement, { + fields: [satisfactionCheckIn.placementId], + references: [placement.id], + }), + collectedByUser: one(user, { + fields: [satisfactionCheckIn.collectedByUserId], + references: [user.id], + relationName: 'CheckInCollectedBy', + }), + appointment: one(appointment, { + fields: [satisfactionCheckIn.appointmentId], + references: [appointment.id], + }), +})) + +export const userRelations = relations(user, ({ one, many }) => ({ + messagesWritten: many(message, { relationName: 'MessageAuthor' }), + auditLogs: many(auditLog), + activitiesCreated: many(activity, { relationName: 'ActivityCreatedBy' }), + activitiesUpdated: many(activity, { relationName: 'ActivityUpdatedBy' }), + careAssignments: many(careAssignment), + appointments: many(appointment), + careAttributesUpdated: many(careAttribute), + houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByStaff' }), + opportunitiesCreated: many(opportunity, { relationName: 'OpportunityCreatedBy' }), + opportunitiesUpdated: many(opportunity, { relationName: 'OpportunityUpdatedBy' }), + applicationsSupported: many(opportunityApplication, { relationName: 'ApplicationSupportedBy' }), + checkInsCollected: many(satisfactionCheckIn, { relationName: 'CheckInCollectedBy' }), + documentsUploaded: many(residentDocument, { relationName: 'DocumentUploadedBy' }), + complaintsAnswered: many(complaint, { relationName: 'ComplaintRespondedBy' }), + account: one(account), +})) + +export const accountRelations = relations(account, ({ one, many }) => ({ + user: one(user, { + fields: [account.userId], + references: [user.id], + }), + resident: one(resident, { + fields: [account.residentId], + references: [resident.id], + }), + authTokens: many(authToken), +})) + +export const authTokenRelations = relations(authToken, ({ one }) => ({ + account: one(account, { + fields: [authToken.accountId], + references: [account.id], + }), +})) + +export const auditLogRelations = relations(auditLog, ({ one }) => ({ + user: one(user, { + fields: [auditLog.userId], + references: [user.id], + }), +})) + +export const activityRelations = relations(activity, ({ one }) => ({ + createdBy: one(user, { + fields: [activity.createdByUserId], + references: [user.id], + relationName: 'ActivityCreatedBy', + }), + updatedBy: one(user, { + fields: [activity.updatedByUserId], + references: [user.id], + relationName: 'ActivityUpdatedBy', + }), +})) + +export const houseEventRelations = relations(houseEvent, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [houseEvent.housingUnitId], + references: [housingUnit.id], + }), + createdByStaff: one(user, { + fields: [houseEvent.createdByStaffId], + references: [user.id], + relationName: 'HouseEventCreatedByStaff', + }), + createdByResident: one(resident, { + fields: [houseEvent.createdByResidentId], + references: [resident.id], + relationName: 'HouseEventCreatedByResident', + }), + rsvps: many(eventRsvp), +})) + +export const eventRsvpRelations = relations(eventRsvp, ({ one }) => ({ + event: one(houseEvent, { + fields: [eventRsvp.eventId], + references: [houseEvent.id], + }), + resident: one(resident, { + fields: [eventRsvp.residentId], + references: [resident.id], + }), +})) + +export const maintenanceRequestRelations = relations(maintenanceRequest, ({ one }) => ({ + housingUnit: one(housingUnit, { + fields: [maintenanceRequest.housingUnitId], + references: [housingUnit.id], + }), + spot: one(placementSpot, { + fields: [maintenanceRequest.spotId], + references: [placementSpot.id], + }), + reportedBy: one(resident, { + fields: [maintenanceRequest.reportedById], + references: [resident.id], + }), +})) + +export const householdTaskRelations = relations(householdTask, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [householdTask.housingUnitId], + references: [housingUnit.id], + }), + createdByResident: one(resident, { + fields: [householdTask.createdByResidentId], + references: [resident.id], + relationName: 'TaskCreator', + }), + completions: many(taskCompletion), + attentionFlags: many(taskAttentionFlag), + requests: many(taskRequest), +})) + +export const taskCompletionRelations = relations(taskCompletion, ({ one, many }) => ({ + task: one(householdTask, { + fields: [taskCompletion.taskId], + references: [householdTask.id], + }), + completedBy: one(resident, { + fields: [taskCompletion.completedById], + references: [resident.id], + }), + resolvedFlags: many(taskAttentionFlag, { relationName: 'FlagResolvedByCompletion' }), + fulfilledRequests: many(taskRequest, { relationName: 'RequestFulfilledByCompletion' }), +})) + +export const taskAttentionFlagRelations = relations(taskAttentionFlag, ({ one }) => ({ + task: one(householdTask, { + fields: [taskAttentionFlag.taskId], + references: [householdTask.id], + }), + flaggedBy: one(resident, { + fields: [taskAttentionFlag.flaggedById], + references: [resident.id], + }), + resolvedByCompletion: one(taskCompletion, { + fields: [taskAttentionFlag.resolvedByCompletionId], + references: [taskCompletion.id], + relationName: 'FlagResolvedByCompletion', + }), +})) + +export const taskRequestRelations = relations(taskRequest, ({ one }) => ({ + task: one(householdTask, { + fields: [taskRequest.taskId], + references: [householdTask.id], + }), + requestedBy: one(resident, { + fields: [taskRequest.requestedById], + references: [resident.id], + relationName: 'TaskRequestsMade', + }), + requestedResident: one(resident, { + fields: [taskRequest.requestedResidentId], + references: [resident.id], + relationName: 'TaskRequestsReceived', + }), + completion: one(taskCompletion, { + fields: [taskRequest.completionId], + references: [taskCompletion.id], + relationName: 'RequestFulfilledByCompletion', + }), +})) + +export const marketplacePostRelations = relations(marketplacePost, ({ one }) => ({ + housingUnit: one(housingUnit, { + fields: [marketplacePost.housingUnitId], + references: [housingUnit.id], + }), + postedBy: one(resident, { + fields: [marketplacePost.postedById], + references: [resident.id], + relationName: 'MarketplacePostedBy', + }), + claimedBy: one(resident, { + fields: [marketplacePost.claimedById], + references: [resident.id], + relationName: 'MarketplacePostClaimedBy', + }), +})) + +export const transferRequestRelations = relations(transferRequest, ({ one }) => ({ + resident: one(resident, { + fields: [transferRequest.residentId], + references: [resident.id], + }), + currentPlacement: one(placement, { + fields: [transferRequest.currentPlacementId], + references: [placement.id], + relationName: 'TransferFromPlacement', + }), + targetUnit: one(housingUnit, { + fields: [transferRequest.targetUnitId], + references: [housingUnit.id], + }), +})) + +export const houseRuleRelations = relations(houseRule, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [houseRule.housingUnitId], + references: [housingUnit.id], + }), + parentRule: one(houseRule, { + fields: [houseRule.parentRuleId], + references: [houseRule.id], + relationName: 'RuleSpecialisation', + }), + childRules: many(houseRule, { relationName: 'RuleSpecialisation' }), + adoptedByProposal: one(proposal, { + fields: [houseRule.adoptedByProposalId], + references: [proposal.id], + relationName: 'ProposalAdoptedRule', + }), + acknowledgements: many(ruleAcknowledgement), + targetedBy: many(proposal, { relationName: 'ProposalTargetRule' }), + topicProposals: many(proposal, { relationName: 'ProposalTopicRule' }), +})) + +export const ruleAcknowledgementRelations = relations(ruleAcknowledgement, ({ one }) => ({ + rule: one(houseRule, { + fields: [ruleAcknowledgement.ruleId], + references: [houseRule.id], + }), + resident: one(resident, { + fields: [ruleAcknowledgement.residentId], + references: [resident.id], + }), +})) + +export const proposalRelations = relations(proposal, ({ one, many }) => ({ + housingUnit: one(housingUnit, { + fields: [proposal.housingUnitId], + references: [housingUnit.id], + }), + targetRule: one(houseRule, { + fields: [proposal.targetRuleId], + references: [houseRule.id], + relationName: 'ProposalTargetRule', + }), + parentOrgRule: one(houseRule, { + fields: [proposal.parentOrgRuleId], + references: [houseRule.id], + relationName: 'ProposalTopicRule', + }), + proposedByResident: one(resident, { + fields: [proposal.proposedByResidentId], + references: [resident.id], + relationName: 'ProposalAuthor', + }), + votes: many(vote), + adoptedRules: many(houseRule, { relationName: 'ProposalAdoptedRule' }), + agreement: one(conflictAgreement), +})) + +export const voteRelations = relations(vote, ({ one }) => ({ + proposal: one(proposal, { + fields: [vote.proposalId], + references: [proposal.id], + }), + resident: one(resident, { + fields: [vote.residentId], + references: [resident.id], + }), +})) + +export const conflictAgreementRelations = relations(conflictAgreement, ({ one, many }) => ({ + incident: one(incident, { + fields: [conflictAgreement.incidentId], + references: [incident.id], + }), + parties: many(agreementParty), + ruleProposal: one(proposal, { + fields: [conflictAgreement.ruleProposalId], + references: [proposal.id], + }), +})) + +export const agreementPartyRelations = relations(agreementParty, ({ one }) => ({ + agreement: one(conflictAgreement, { + fields: [agreementParty.agreementId], + references: [conflictAgreement.id], + }), + resident: one(resident, { + fields: [agreementParty.residentId], + references: [resident.id], + }), +})) + +export const learningRecordRelations = relations(learningRecord, ({ one }) => ({ + resident: one(resident, { + fields: [learningRecord.residentId], + references: [resident.id], + }), + fromApplication: one(opportunityApplication), +})) + +export const careAssignmentRelations = relations(careAssignment, ({ one }) => ({ + resident: one(resident, { + fields: [careAssignment.residentId], + references: [resident.id], + }), + staff: one(user, { + fields: [careAssignment.staffId], + references: [user.id], + }), +})) + +export const appointmentRelations = relations(appointment, ({ one }) => ({ + resident: one(resident, { + fields: [appointment.residentId], + references: [resident.id], + }), + staff: one(user, { + fields: [appointment.staffId], + references: [user.id], + }), + checkIn: one(satisfactionCheckIn), +})) + +export const careAttributeRelations = relations(careAttribute, ({ one }) => ({ + resident: one(resident, { + fields: [careAttribute.residentId], + references: [resident.id], + }), + updatedBy: one(user, { + fields: [careAttribute.updatedById], + references: [user.id], + }), +})) + +export const opportunityRelations = relations(opportunity, ({ one, many }) => ({ + createdBy: one(user, { + fields: [opportunity.createdByUserId], + references: [user.id], + relationName: 'OpportunityCreatedBy', + }), + updatedBy: one(user, { + fields: [opportunity.updatedByUserId], + references: [user.id], + relationName: 'OpportunityUpdatedBy', + }), + applications: many(opportunityApplication), +})) + +export const opportunityApplicationRelations = relations(opportunityApplication, ({ one }) => ({ + resident: one(resident, { + fields: [opportunityApplication.residentId], + references: [resident.id], + }), + opportunity: one(opportunity, { + fields: [opportunityApplication.opportunityId], + references: [opportunity.id], + }), + supportedBy: one(user, { + fields: [opportunityApplication.supportedByUserId], + references: [user.id], + relationName: 'ApplicationSupportedBy', + }), + learningRecord: one(learningRecord, { + fields: [opportunityApplication.learningRecordId], + references: [learningRecord.id], + }), +})) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts new file mode 100644 index 00000000..22694cea --- /dev/null +++ b/src/lib/db/schema.ts @@ -0,0 +1,1515 @@ +/** + * Drizzle schema — SSOT for tables, enums and (via types.ts) derived types. + * + * EXACT parity with the live database, whose shape was created by the 29 + * Prisma migrations in the repo's history: table/column names are Prisma's + * PascalCase/camelCase ("PlacementSpot", "createdAt"), enums keep their + * Prisma type names ("AgeRange"), ids are TEXT minted app-side, timestamps + * are TIMESTAMP(3), and every index/FK/unique constraint name is pinned to + * the one Prisma generated ("Resident_code_key", "Placement_residentId_fkey", + * …) so a fresh database and the live one are byte-identical. Do not + * "normalize" any of this — the schema must match the database that exists, + * and the deploy pipeline refuses destructive diffs. + * + * Prisma generated cuid() ids and @updatedAt client-side; the equivalents + * here are $defaultFn(createId) (@paralleldrive/cuid2) and + * $defaultFn/$onUpdateFn — also app-side, so the columns carry no new DB + * defaults. Initially generated by drizzle-kit pull against a database built + * by the Prisma migration chain, then verified by a normalized pg_dump diff + * (see PR #). + */ + +import { createId } from '@paralleldrive/cuid2' +import { sql } from 'drizzle-orm' +import { customType, pgTable, index, text, timestamp, doublePrecision, jsonb, integer, boolean, uniqueIndex, foreignKey, type AnyPgColumn, unique, check, pgEnum } from "drizzle-orm/pg-core" + +// Prisma's `Bytes` -> Postgres bytea. drizzle-orm has no built-in bytea type. +const bytea = customType<{ data: Buffer; driverData: Buffer }>({ + dataType() { + return 'bytea' + }, +}) + +export const activityCategory = pgEnum("ActivityCategory", ['SPORT', 'LANGUAGE', 'CULTURE', 'COMMUNITY', 'FAMILY', 'SUPPORT']) +export const activityCost = pgEnum("ActivityCost", ['FREE', 'REDUCED', 'PAID']) +export const activityStatus = pgEnum("ActivityStatus", ['DRAFT', 'PUBLISHED', 'ARCHIVED']) +export const ageRange = pgEnum("AgeRange", ['YOUNG_ADULT', 'ADULT', 'MIDDLE_AGED', 'SENIOR']) +export const agreementStatus = pgEnum("AgreementStatus", ['PROPOSED', 'ACCEPTED', 'HELD', 'BROKEN', 'EXPIRED']) +export const applicationStage = pgEnum("ApplicationStage", ['INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED', 'STARTED', 'ENDED', 'DECLINED']) +export const appointmentStatus = pgEnum("AppointmentStatus", ['SCHEDULED', 'COMPLETED', 'CANCELLED', 'NO_SHOW', 'REQUESTED']) +export const authTokenPurpose = pgEnum("AuthTokenPurpose", ['VERIFY_EMAIL', 'RESET_PASSWORD']) +export const careRole = pgEnum("CareRole", ['HOUSING', 'SOCIAL', 'JOB', 'VOLUNTEERING']) +export const checkInType = pgEnum("CheckInType", ['INITIAL', 'REGULAR', 'AD_HOC', 'EXIT']) +export const complaintStatus = pgEnum("ComplaintStatus", ['OPEN', 'IN_REVIEW', 'ANSWERED']) +export const complaintSubject = pgEnum("ComplaintSubject", ['STAFF', 'ACCOMMODATION', 'DECISION', 'OTHER']) +export const conflictStyle = pgEnum("ConflictStyle", ['AVOIDANT', 'COOPERATIVE', 'DIRECT']) +export const decisionMode = pgEnum("DecisionMode", ['RESIDENT_BINDING', 'RESIDENT_ADVISORY', 'STAFF_ONLY']) +export const endReason = pgEnum("EndReason", ['NATURAL', 'CONFLICT', 'REQUEST', 'CAPACITY', 'UPGRADE', 'OTHER']) +export const eventRsvpStatus = pgEnum("EventRsvpStatus", ['GOING', 'MAYBE', 'DECLINED']) +export const familyStatus = pgEnum("FamilyStatus", ['SINGLE', 'COUPLE', 'FAMILY_WITH_CHILDREN', 'SINGLE_PARENT']) +export const followUpPriority = pgEnum("FollowUpPriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) +export const gender = pgEnum("Gender", ['MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_SAY']) +export const houseEventCategory = pgEnum("HouseEventCategory", ['HOUSE_MEETING', 'SOCIAL', 'CULTURE', 'SUPPORT']) +export const houseEventStatus = pgEnum("HouseEventStatus", ['DRAFT', 'PUBLISHED', 'CANCELLED']) +export const householdTaskCategory = pgEnum("HouseholdTaskCategory", ['CLEANING', 'SHOPPING', 'MAINTENANCE', 'COOKING', 'TRASH', 'OTHER']) +export const householdTaskPriority = pgEnum("HouseholdTaskPriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) +export const householdTaskStatus = pgEnum("HouseholdTaskStatus", ['IDLE', 'NEEDS_ATTENTION', 'REQUESTED', 'IN_PROGRESS']) +export const householdTaskType = pgEnum("HouseholdTaskType", ['ONE_TIME', 'RECURRING_SCHEDULED', 'RECURRING_AS_NEEDED']) +export const housingStatus = pgEnum("HousingStatus", ['AVAILABLE', 'FULL', 'MAINTENANCE', 'CLOSED']) +export const incidentCategory = pgEnum("IncidentCategory", ['INTERPERSONAL', 'MAINTENANCE', 'SAFETY', 'WELLBEING']) +export const incidentSeverity = pgEnum("IncidentSeverity", ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']) +export const incidentType = pgEnum("IncidentType", ['NOISE_COMPLAINT', 'CLEANLINESS_DISPUTE', 'PERSONAL_CONFLICT', 'CULTURAL_FRICTION', 'SPACE_DISPUTE', 'SCHEDULE_CONFLICT', 'SAFETY_CONCERN', 'PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY_SYSTEM', 'GENERAL_MAINTENANCE', 'LOW_SATISFACTION', 'OTHER']) +export const involvementRole = pgEnum("InvolvementRole", ['INVOLVED', 'WITNESS', 'MEDIATOR']) +export const learningKind = pgEnum("LearningKind", ['LANGUAGE_TEST', 'COURSE', 'INFORMAL', 'QUALIFICATION', 'VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP']) +export const learningStatus = pgEnum("LearningStatus", ['PLANNED', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED']) +export const livingSkillsSupport = pgEnum("LivingSkillsSupport", ['INDEPENDENT', 'SOME_SUPPORT', 'REGULAR_SUPPORT']) +export const maintenanceCategory = pgEnum("MaintenanceCategory", ['PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY', 'CLEANING', 'EXTERIOR', 'OTHER']) +export const maintenancePriority = pgEnum("MaintenancePriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) +export const maintenanceStatus = pgEnum("MaintenanceStatus", ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD', 'COMPLETED', 'CANCELLED']) +export const marketplacePostKind = pgEnum("MarketplacePostKind", ['GIVE_AWAY', 'LEND', 'WANTED', 'OFFER_HELP', 'NEED_HELP']) +export const marketplacePostStatus = pgEnum("MarketplacePostStatus", ['OPEN', 'CLAIMED', 'CLOSED']) +export const medicalDocType = pgEnum("MedicalDocType", ['PRIVATE_ROOM', 'STUDIO', 'BOTH']) +export const mobilityNeed = pgEnum("MobilityNeed", ['NONE', 'GROUND_FLOOR', 'WHEELCHAIR']) +export const opportunityKind = pgEnum("OpportunityKind", ['VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP']) +export const opportunityStatus = pgEnum("OpportunityStatus", ['DRAFT', 'PUBLISHED', 'ARCHIVED']) +export const permitRequirement = pgEnum("PermitRequirement", ['NONE', 'EMPLOYER_NOTIFIES', 'PERMIT_REQUIRED']) +export const placementStatus = pgEnum("PlacementStatus", ['ACTIVE', 'ENDED', 'TRANSFERRED']) +export const profileVisibility = pgEnum("ProfileVisibility", ['PRIVATE', 'ROOMMATES', 'RESIDENTS']) +export const proposalStatus = pgEnum("ProposalStatus", ['DISCUSSION', 'VOTING', 'NEEDS_STAFF_CONFIRMATION', 'ACCEPTED', 'REJECTED', 'WITHDRAWN', 'VETOED', 'EXPIRED']) +export const proposalType = pgEnum("ProposalType", ['ADD_RULE', 'AMEND_RULE', 'REPEAL_RULE', 'HOUSE_DECISION']) +export const recyclingKnowledge = pgEnum("RecyclingKnowledge", ['NONE', 'BASIC', 'GOOD']) +export const residentOrStaff = pgEnum("ResidentOrStaff", ['RESIDENT', 'STAFF']) +export const residentStatus = pgEnum("ResidentStatus", ['ACTIVE', 'PLACED', 'TRANSFERRED', 'EXITED']) +export const resolutionStage = pgEnum("ResolutionStage", ['REPORTED', 'SELF_RESOLUTION', 'PEER_MEDIATION', 'STAFF_MEDIATION', 'FORMAL_MEASURE', 'CLOSED']) +export const roomSharingStatus = pgEnum("RoomSharingStatus", ['CAN_SHARE', 'PREFERS_PRIVATE', 'NEEDS_PRIVATE']) +export const ruleCategory = pgEnum("RuleCategory", ['SAFETY', 'RESPECT', 'NOISE', 'CLEANLINESS', 'KITCHEN', 'BATHROOM', 'GUESTS', 'SHARED_SPACES', 'COSTS', 'COMMUNICATION', 'OTHER']) +export const ruleDelegation = pgEnum("RuleDelegation", ['FIXED', 'UNIT_MAY_STRENGTHEN', 'UNIT_DECIDES']) +export const ruleScope = pgEnum("RuleScope", ['ORG', 'UNIT']) +export const ruleStatus = pgEnum("RuleStatus", ['ACTIVE', 'SUPERSEDED', 'ARCHIVED']) +export const sleepSchedule = pgEnum("SleepSchedule", ['EARLY_BIRD', 'STANDARD', 'NIGHT_OWL', 'IRREGULAR']) +export const smokingStatus = pgEnum("SmokingStatus", ['NON_SMOKER', 'OUTDOOR_SMOKER', 'INDOOR_SMOKER']) +export const socialStyle = pgEnum("SocialStyle", ['INTROVERTED', 'MODERATE', 'EXTROVERTED']) +export const spotStatus = pgEnum("SpotStatus", ['AVAILABLE', 'OCCUPIED', 'MAINTENANCE', 'CLOSED']) +export const spotType = pgEnum("SpotType", ['BED', 'PRIVATE_ROOM', 'STUDIO', 'ROOM']) +export const staffDecision = pgEnum("StaffDecision", ['CONFIRMED', 'VETOED']) +export const staffRole = pgEnum("StaffRole", ['ADMIN', 'BETREUUNG', 'SOZIALARBEIT', 'JOBCOACH', 'FREIWILLIGENARBEIT']) +export const staffScope = pgEnum("StaffScope", ['OWN_DOMAIN', 'ALL_DOMAINS']) +export const supportLevel = pgEnum("SupportLevel", ['STANDARD', 'ELEVATED', 'INTENSIVE']) +export const taskRequestStatus = pgEnum("TaskRequestStatus", ['PENDING', 'ACCEPTED', 'DECLINED', 'COMPLETED']) +export const transferRequestStatus = pgEnum("TransferRequestStatus", ['PENDING', 'APPROVED', 'DENIED', 'COMPLETED', 'CANCELLED']) +export const voteChoice = pgEnum("VoteChoice", ['YES', 'NO', 'ABSTAIN', 'BLOCK']) +export const voteThreshold = pgEnum("VoteThreshold", ['CONSENSUS', 'SUPERMAJORITY', 'SIMPLE_MAJORITY']) + + +export const algorithmWeight = pgTable("AlgorithmWeight", { + id: text().primaryKey().$defaultFn(createId).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + lifestyleWeight: doublePrecision().default(30).notNull(), + socialWeight: doublePrecision().default(25).notNull(), + practicalWeight: doublePrecision().default(25).notNull(), + riskWeight: doublePrecision().default(20).notNull(), + factorWeights: jsonb().notNull(), + version: integer().default(1).notNull(), + active: boolean().default(true).notNull(), + notes: text(), +}, (table) => [ + index("AlgorithmWeight_active_idx").using("btree", table.active.asc().nullsLast()), +]); + +export const placementSpot = pgTable("PlacementSpot", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + code: text().notNull(), + label: text(), + type: spotType().notNull(), + parentSpotId: text(), + squareMeters: doublePrecision(), + floor: integer(), + hasPrivateBathroom: boolean().default(false).notNull(), + hasPrivateKitchen: boolean().default(false).notNull(), + hasPrivateToilet: boolean().default(false).notNull(), + capacity: integer().default(1).notNull(), + requiresMedicalDocs: boolean().default(false).notNull(), + status: spotStatus().default('AVAILABLE').notNull(), + notes: text(), +}, (table) => [ + uniqueIndex("PlacementSpot_housingUnitId_code_key").using("btree", table.housingUnitId.asc().nullsLast(), table.code.asc().nullsLast()), + index("PlacementSpot_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), + index("PlacementSpot_requiresMedicalDocs_idx").using("btree", table.requiresMedicalDocs.asc().nullsLast()), + index("PlacementSpot_type_status_idx").using("btree", table.type.asc().nullsLast(), table.status.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "PlacementSpot_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.parentSpotId], + foreignColumns: [table.id], + name: "PlacementSpot_parentSpotId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const placement = pgTable("Placement", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + housingUnitId: text().notNull(), + spotId: text(), + startDate: timestamp({ precision: 3, mode: 'date' }).notNull(), + endDate: timestamp({ precision: 3, mode: 'date' }), + compatibilityScore: doublePrecision(), + lifestyleScore: doublePrecision(), + socialScore: doublePrecision(), + practicalScore: doublePrecision(), + riskScore: doublePrecision(), + status: placementStatus().default('ACTIVE').notNull(), + endReason: endReason(), + satisfactionRating: integer(), + placementNotes: text(), + outcomeNotes: text(), + conflictGap: text(), + wasPredictable: boolean(), + relatedIncidentId: text(), +}, (table) => [ + index("Placement_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), + uniqueIndex("Placement_residentId_housingUnitId_startDate_key").using("btree", table.residentId.asc().nullsLast(), table.housingUnitId.asc().nullsLast(), table.startDate.asc().nullsLast()), + index("Placement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + index("Placement_startDate_endDate_idx").using("btree", table.startDate.asc().nullsLast(), table.endDate.asc().nullsLast()), + index("Placement_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "Placement_residentId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "Placement_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.spotId], + foreignColumns: [placementSpot.id], + name: "Placement_spotId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.relatedIncidentId], + foreignColumns: [incident.id], + name: "Placement_relatedIncidentId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const compatibilityAssessment = pgTable("CompatibilityAssessment", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + residentId: text().notNull(), + comparedWithId: text().notNull(), + overallScore: doublePrecision().notNull(), + lifestyleScore: doublePrecision().notNull(), + socialScore: doublePrecision().notNull(), + practicalScore: doublePrecision().notNull(), + riskScore: doublePrecision().notNull(), + strengths: text().array(), + concerns: text().array(), + recommendations: text().array(), +}, (table) => [ + index("CompatibilityAssessment_overallScore_idx").using("btree", table.overallScore.asc().nullsLast()), + uniqueIndex("CompatibilityAssessment_residentId_comparedWithId_key").using("btree", table.residentId.asc().nullsLast(), table.comparedWithId.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "CompatibilityAssessment_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.comparedWithId], + foreignColumns: [resident.id], + name: "CompatibilityAssessment_comparedWithId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const incidentFollowUp = pgTable("IncidentFollowUp", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + incidentId: text().notNull(), + action: text().notNull(), + notes: text(), + outcome: text(), + staffName: text(), + scheduledNextDate: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + index("IncidentFollowUp_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), + index("IncidentFollowUp_incidentId_idx").using("btree", table.incidentId.asc().nullsLast()), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: "IncidentFollowUp_incidentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const incidentInvolvement = pgTable("IncidentInvolvement", { + id: text().primaryKey().$defaultFn(createId).notNull(), + incidentId: text().notNull(), + residentId: text().notNull(), + role: involvementRole().default('INVOLVED').notNull(), +}, (table) => [ + uniqueIndex("IncidentInvolvement_incidentId_residentId_key").using("btree", table.incidentId.asc().nullsLast(), table.residentId.asc().nullsLast()), + index("IncidentInvolvement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: "IncidentInvolvement_incidentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "IncidentInvolvement_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const auditLog = pgTable("AuditLog", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + action: text().notNull(), + entity: text().notNull(), + entityId: text().notNull(), + userId: text(), + changes: jsonb(), + reason: text(), +}, (table) => [ + index("AuditLog_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), + index("AuditLog_entity_entityId_idx").using("btree", table.entity.asc().nullsLast(), table.entityId.asc().nullsLast()), + index("AuditLog_userId_idx").using("btree", table.userId.asc().nullsLast()), + foreignKey({ + columns: [table.userId], + foreignColumns: [user.id], + name: "AuditLog_userId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const maintenanceRequest = pgTable("MaintenanceRequest", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + spotId: text(), + category: maintenanceCategory().notNull(), + priority: maintenancePriority().default('NORMAL').notNull(), + title: text().notNull(), + description: text().notNull(), + location: text(), + reportedById: text(), + reporterName: text(), + assignedTo: text(), + assignedAt: timestamp({ precision: 3, mode: 'date' }), + status: maintenanceStatus().default('OPEN').notNull(), + startedAt: timestamp({ precision: 3, mode: 'date' }), + completedAt: timestamp({ precision: 3, mode: 'date' }), + resolution: text(), + cost: doublePrecision(), + notes: text(), +}, (table) => [ + index("MaintenanceRequest_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), + index("MaintenanceRequest_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), + index("MaintenanceRequest_priority_status_idx").using("btree", table.priority.asc().nullsLast(), table.status.asc().nullsLast()), + index("MaintenanceRequest_reportedById_idx").using("btree", table.reportedById.asc().nullsLast()), + index("MaintenanceRequest_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "MaintenanceRequest_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.spotId], + foreignColumns: [placementSpot.id], + name: "MaintenanceRequest_spotId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.reportedById], + foreignColumns: [resident.id], + name: "MaintenanceRequest_reportedById_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const taskAttentionFlag = pgTable("TaskAttentionFlag", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + taskId: text().notNull(), + flaggedById: text().notNull(), + message: text(), + isResolved: boolean().default(false).notNull(), + resolvedAt: timestamp({ precision: 3, mode: 'date' }), + resolvedByCompletionId: text(), +}, (table) => [ + index("TaskAttentionFlag_taskId_idx").using("btree", table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: "TaskAttentionFlag_taskId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.flaggedById], + foreignColumns: [resident.id], + name: "TaskAttentionFlag_flaggedById_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.resolvedByCompletionId], + foreignColumns: [taskCompletion.id], + name: "TaskAttentionFlag_resolvedByCompletionId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const housingUnit = pgTable("HousingUnit", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + code: text().notNull(), + address: text().notNull(), + totalBeds: integer().notNull(), + totalRooms: integer().notNull(), + sharedRooms: integer().notNull(), + privateRooms: integer().notNull(), + sharedBathrooms: integer().notNull(), + privateBathrooms: integer().notNull(), + sharedKitchen: boolean().default(true).notNull(), + privateKitchen: boolean().default(false).notNull(), + groundFloor: boolean().default(false).notNull(), + wheelchairAccess: boolean().default(false).notNull(), + elevator: boolean().default(false).notNull(), + smokingAllowed: boolean().default(false).notNull(), + petsAllowed: boolean().default(false).notNull(), + quietHours: text(), + nearPublicTransport: boolean().default(true).notNull(), + nearHealthServices: boolean().default(false).notNull(), + nearSchools: boolean().default(false).notNull(), + status: housingStatus().default('AVAILABLE').notNull(), + notes: text(), + nickname: text(), + buildingCode: text(), +}, (table) => [ + index("HousingUnit_buildingCode_idx").using("btree", table.buildingCode.asc().nullsLast()), + uniqueIndex("HousingUnit_code_key").using("btree", table.code.asc().nullsLast()), + index("HousingUnit_status_idx").using("btree", table.status.asc().nullsLast()), + index("HousingUnit_totalBeds_idx").using("btree", table.totalBeds.asc().nullsLast()), +]); + +export const householdTask = pgTable("HouseholdTask", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + title: text().notNull(), + description: text(), + instructions: text(), + taskType: householdTaskType().default('ONE_TIME').notNull(), + category: householdTaskCategory().default('OTHER').notNull(), + priority: householdTaskPriority().default('NORMAL').notNull(), + scheduleHuman: text(), + estimatedMinutes: integer(), + currentStatus: householdTaskStatus().default('IDLE').notNull(), + isCompleted: boolean().default(false).notNull(), + completedAt: timestamp({ precision: 3, mode: 'date' }), + createdByResidentId: text(), + createdByStaff: text(), + checklist: text().array().default(sql`ARRAY[]::TEXT[]`), + rotationResidentIds: text().array().default(sql`ARRAY[]::TEXT[]`), +}, (table) => [ + index("HouseholdTask_housingUnitId_category_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.category.asc().nullsLast()), + index("HouseholdTask_housingUnitId_currentStatus_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.currentStatus.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "HouseholdTask_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.createdByResidentId], + foreignColumns: [resident.id], + name: "HouseholdTask_createdByResidentId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const satisfactionCheckIn = pgTable("SatisfactionCheckIn", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + placementId: text().notNull(), + checkInType: checkInType().notNull(), + weekNumber: integer(), + overallSatisfaction: integer().notNull(), + roommateRelations: integer(), + facilitySatisfaction: integer(), + safetyFeeling: integer(), + concerns: text(), + improvements: text(), + positives: text(), + collectedBy: text(), + isAnonymous: boolean().default(false).notNull(), + appointmentId: text(), + collectedByUserId: text(), +}, (table) => [ + uniqueIndex("SatisfactionCheckIn_appointmentId_key").using("btree", table.appointmentId.asc().nullsLast()), + index("SatisfactionCheckIn_checkInType_idx").using("btree", table.checkInType.asc().nullsLast()), + index("SatisfactionCheckIn_collectedByUserId_idx").using("btree", table.collectedByUserId.asc().nullsLast()), + index("SatisfactionCheckIn_placementId_idx").using("btree", table.placementId.asc().nullsLast()), + foreignKey({ + columns: [table.placementId], + foreignColumns: [placement.id], + name: "SatisfactionCheckIn_placementId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.appointmentId], + foreignColumns: [appointment.id], + name: "SatisfactionCheckIn_appointmentId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.collectedByUserId], + foreignColumns: [user.id], + name: "SatisfactionCheckIn_collectedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const user = pgTable("User", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + name: text().notNull(), + role: staffRole().default('BETREUUNG').notNull(), + active: boolean().default(true).notNull(), + lastLoginAt: timestamp({ precision: 3, mode: 'date' }), + code: text().notNull(), + scope: staffScope().default('OWN_DOMAIN').notNull(), + isSystemAdmin: boolean().default(false).notNull(), +}, (table) => [ + index("User_code_idx").using("btree", table.code.asc().nullsLast()), + index("User_role_idx").using("btree", table.role.asc().nullsLast()), + index("User_scope_idx").using("btree", table.scope.asc().nullsLast()), + unique("User_code_key").on(table.code), +]); + +export const taskRequest = pgTable("TaskRequest", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + taskId: text().notNull(), + requestedById: text().notNull(), + requestedResidentId: text(), + isBroadcast: boolean().default(false).notNull(), + message: text(), + status: taskRequestStatus().default('PENDING').notNull(), + responseMessage: text(), + completionId: text(), +}, (table) => [ + index("TaskRequest_requestedResidentId_idx").using("btree", table.requestedResidentId.asc().nullsLast()), + index("TaskRequest_taskId_idx").using("btree", table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: "TaskRequest_taskId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.requestedById], + foreignColumns: [resident.id], + name: "TaskRequest_requestedById_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.requestedResidentId], + foreignColumns: [resident.id], + name: "TaskRequest_requestedResidentId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.completionId], + foreignColumns: [taskCompletion.id], + name: "TaskRequest_completionId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const transferRequest = pgTable("TransferRequest", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + currentPlacementId: text(), + targetUnitId: text(), + reason: text().notNull(), + status: transferRequestStatus().default('PENDING').notNull(), + staffNotes: text(), + reviewedBy: text(), + reviewedAt: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + index("TransferRequest_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + index("TransferRequest_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "TransferRequest_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.currentPlacementId], + foreignColumns: [placement.id], + name: "TransferRequest_currentPlacementId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.targetUnitId], + foreignColumns: [housingUnit.id], + name: "TransferRequest_targetUnitId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const resident = pgTable("Resident", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + code: text().notNull(), + ageRange: ageRange().notNull(), + gender: gender().notNull(), + familyStatus: familyStatus().notNull(), + sleepSchedule: sleepSchedule().notNull(), + noiseTolerance: integer().notNull(), + cleanlinessPractice: integer().notNull(), + guestTolerance: integer().default(3).notNull(), + socialStyle: socialStyle().notNull(), + languages: text().array(), + culturalRegion: text(), + conflictStyle: conflictStyle().default('COOPERATIVE').notNull(), + smokingStatus: smokingStatus().notNull(), + dietaryNeeds: text().array(), + mobilityNeeds: mobilityNeed().notNull(), + medicalEquipment: boolean().default(false).notNull(), + petTolerance: boolean().default(true).notNull(), + sharedBathroom: boolean().default(true).notNull(), + sharedKitchen: boolean().default(true).notNull(), + privacyNeed: integer().notNull(), + choresContribution: integer().default(3).notNull(), + recyclingKnowledge: recyclingKnowledge().default('NONE').notNull(), + roomSharingStatus: roomSharingStatus().default('CAN_SHARE').notNull(), + hasNightDisturbances: boolean().default(false).notNull(), + needsQuietEnvironment: boolean().default(false).notNull(), + hasSleepEquipment: boolean().default(false).notNull(), + supportLevel: supportLevel().default('STANDARD').notNull(), + roommatePreferences: text(), + status: residentStatus().default('ACTIVE').notNull(), + notes: text(), + hasMedicalDocumentation: boolean().default(false).notNull(), + medicalDocType: medicalDocType(), + medicalDocDate: timestamp({ precision: 3, mode: 'date' }), + medicalDocNotes: text(), + preferencesCompletedAt: timestamp({ precision: 3, mode: 'date' }), + cleanlinessExpectation: integer().default(3).notNull(), + chaosTolerance: integer().default(3).notNull(), + bio: text(), + displayName: text(), + profileVisibility: profileVisibility().default('ROOMMATES').notNull(), + livingSkillsSupport: livingSkillsSupport().default('INDEPENDENT').notNull(), +}, (table) => [ + index("Resident_ageRange_gender_idx").using("btree", table.ageRange.asc().nullsLast(), table.gender.asc().nullsLast()), + uniqueIndex("Resident_code_key").using("btree", table.code.asc().nullsLast()), + index("Resident_livingSkillsSupport_idx").using("btree", table.livingSkillsSupport.asc().nullsLast()), + index("Resident_status_idx").using("btree", table.status.asc().nullsLast()), +]); + +export const activity = pgTable("Activity", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + title: text().notNull(), + description: text().notNull(), + category: activityCategory().notNull(), + cost: activityCost().default('FREE').notNull(), + costNote: text(), + location: text(), + website: text(), + phone: text(), + schedule: text(), + startsAt: timestamp({ precision: 3, mode: 'date' }), + endsAt: timestamp({ precision: 3, mode: 'date' }), + status: activityStatus().default('DRAFT').notNull(), + highlight: boolean().default(false).notNull(), + createdByUserId: text(), + updatedByUserId: text(), +}, (table) => [ + index("Activity_endsAt_idx").using("btree", table.endsAt.asc().nullsLast()), + index("Activity_status_category_idx").using("btree", table.status.asc().nullsLast(), table.category.asc().nullsLast()), + index("Activity_status_highlight_idx").using("btree", table.status.asc().nullsLast(), table.highlight.asc().nullsLast()), + foreignKey({ + columns: [table.createdByUserId], + foreignColumns: [user.id], + name: "Activity_createdByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.updatedByUserId], + foreignColumns: [user.id], + name: "Activity_updatedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const systemConfig = pgTable("SystemConfig", { + id: text().default('singleton').primaryKey().notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + pilotBaselineIncidentsPerMonth: doublePrecision(), + pilotBaselineRelocationsPerMonth: doublePrecision(), + pilotBaselineMediationHoursPerWeek: doublePrecision(), + pilotStartDate: timestamp({ precision: 3, mode: 'date' }), +}); + +export const incident = pgTable("Incident", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + placementId: text(), + reportedById: text(), + subjectId: text(), + date: timestamp({ precision: 3, mode: 'date' }).notNull(), + category: incidentCategory().default('INTERPERSONAL').notNull(), + type: incidentType().notNull(), + severity: incidentSeverity().notNull(), + description: text().notNull(), + resolution: text(), + resolvedAt: timestamp({ precision: 3, mode: 'date' }), + predictable: boolean(), + compatibilityGap: text(), + nextFollowUpDate: timestamp({ precision: 3, mode: 'date' }), + followUpPriority: followUpPriority(), + mediationMinutes: integer(), + resolutionStage: resolutionStage().default('REPORTED').notNull(), + stageEnteredAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), +}, (table) => [ + index("Incident_date_idx").using("btree", table.date.asc().nullsLast()), + index("Incident_nextFollowUpDate_idx").using("btree", table.nextFollowUpDate.asc().nullsLast()), + index("Incident_reportedById_idx").using("btree", table.reportedById.asc().nullsLast()), + index("Incident_subjectId_idx").using("btree", table.subjectId.asc().nullsLast()), + index("Incident_type_severity_idx").using("btree", table.type.asc().nullsLast(), table.severity.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "Incident_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.placementId], + foreignColumns: [placement.id], + name: "Incident_placementId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.reportedById], + foreignColumns: [resident.id], + name: "Incident_reportedById_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.subjectId], + foreignColumns: [resident.id], + name: "Incident_subjectId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const houseRule = pgTable("HouseRule", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + scope: ruleScope().notNull(), + housingUnitId: text(), + key: text(), + category: ruleCategory().notNull(), + title: text().notNull(), + body: text().notNull(), + delegation: ruleDelegation().default('FIXED').notNull(), + parentRuleId: text(), + status: ruleStatus().default('ACTIVE').notNull(), + version: integer().default(1).notNull(), + effectiveFrom: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + effectiveUntil: timestamp({ precision: 3, mode: 'date' }), + adoptedByProposalId: text(), + createdByStaff: text(), +}, (table) => [ + index("HouseRule_category_idx").using("btree", table.category.asc().nullsLast()), + index("HouseRule_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), + uniqueIndex("HouseRule_key_key").using("btree", table.key.asc().nullsLast()), + index("HouseRule_parentRuleId_idx").using("btree", table.parentRuleId.asc().nullsLast()), + index("HouseRule_scope_status_idx").using("btree", table.scope.asc().nullsLast(), table.status.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "HouseRule_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.parentRuleId], + foreignColumns: [table.id], + name: "HouseRule_parentRuleId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.adoptedByProposalId], + foreignColumns: [proposal.id], + name: "HouseRule_adoptedByProposalId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const proposal = pgTable("Proposal", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + type: proposalType().notNull(), + category: ruleCategory().notNull(), + title: text().notNull(), + body: text().notNull(), + targetRuleId: text(), + parentOrgRuleId: text(), + proposedByResidentId: text(), + proposedByStaff: text(), + status: proposalStatus().default('DISCUSSION').notNull(), + decisionMode: decisionMode().notNull(), + threshold: voteThreshold().notNull(), + quorumPercent: integer().notNull(), + approvalPercent: integer().notNull(), + eligibleVoterCount: integer().default(0).notNull(), + discussionEndsAt: timestamp({ precision: 3, mode: 'date' }), + votingOpenedAt: timestamp({ precision: 3, mode: 'date' }), + votingEndsAt: timestamp({ precision: 3, mode: 'date' }), + decidedAt: timestamp({ precision: 3, mode: 'date' }), + outcomeSummary: text(), + staffDecision: staffDecision(), + staffNotes: text(), + staffUserId: text(), + staffDecidedAt: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + index("Proposal_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), + index("Proposal_status_votingEndsAt_idx").using("btree", table.status.asc().nullsLast(), table.votingEndsAt.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "Proposal_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.targetRuleId], + foreignColumns: [houseRule.id], + name: "Proposal_targetRuleId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.parentOrgRuleId], + foreignColumns: [houseRule.id], + name: "Proposal_parentOrgRuleId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.proposedByResidentId], + foreignColumns: [resident.id], + name: "Proposal_proposedByResidentId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const ruleAcknowledgement = pgTable("RuleAcknowledgement", { + id: text().primaryKey().$defaultFn(createId).notNull(), + ruleId: text().notNull(), + residentId: text().notNull(), + ruleVersion: integer().notNull(), + acknowledgedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), +}, (table) => [ + index("RuleAcknowledgement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + uniqueIndex("RuleAcknowledgement_ruleId_residentId_ruleVersion_key").using("btree", table.ruleId.asc().nullsLast(), table.residentId.asc().nullsLast(), table.ruleVersion.asc().nullsLast()), + foreignKey({ + columns: [table.ruleId], + foreignColumns: [houseRule.id], + name: "RuleAcknowledgement_ruleId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "RuleAcknowledgement_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const vote = pgTable("Vote", { + id: text().primaryKey().$defaultFn(createId).notNull(), + proposalId: text().notNull(), + residentId: text().notNull(), + choice: voteChoice().notNull(), + reason: text(), + castAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), +}, (table) => [ + uniqueIndex("Vote_proposalId_residentId_key").using("btree", table.proposalId.asc().nullsLast(), table.residentId.asc().nullsLast()), + index("Vote_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.proposalId], + foreignColumns: [proposal.id], + name: "Vote_proposalId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "Vote_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const conflictAgreement = pgTable("ConflictAgreement", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + incidentId: text().notNull(), + terms: text().notNull(), + mediatorName: text(), + reviewDate: timestamp({ precision: 3, mode: 'date' }).notNull(), + status: agreementStatus().default('PROPOSED').notNull(), + outcomeNotes: text(), + reviewedAt: timestamp({ precision: 3, mode: 'date' }), + ruleProposalId: text(), +}, (table) => [ + index("ConflictAgreement_incidentId_idx").using("btree", table.incidentId.asc().nullsLast()), + uniqueIndex("ConflictAgreement_ruleProposalId_key").using("btree", table.ruleProposalId.asc().nullsLast()), + index("ConflictAgreement_status_reviewDate_idx").using("btree", table.status.asc().nullsLast(), table.reviewDate.asc().nullsLast()), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: "ConflictAgreement_incidentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.ruleProposalId], + foreignColumns: [proposal.id], + name: "ConflictAgreement_ruleProposalId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const agreementParty = pgTable("AgreementParty", { + id: text().primaryKey().$defaultFn(createId).notNull(), + agreementId: text().notNull(), + residentId: text().notNull(), + acceptedAt: timestamp({ precision: 3, mode: 'date' }), + declinedAt: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + uniqueIndex("AgreementParty_agreementId_residentId_key").using("btree", table.agreementId.asc().nullsLast(), table.residentId.asc().nullsLast()), + index("AgreementParty_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.agreementId], + foreignColumns: [conflictAgreement.id], + name: "AgreementParty_agreementId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "AgreementParty_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const residentPhoto = pgTable("ResidentPhoto", { + residentId: text().primaryKey().notNull(), + data: bytea().notNull(), + mimeType: text().notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), +}, (table) => [ + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "ResidentPhoto_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const expense = pgTable("Expense", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + paidById: text().notNull(), + createdById: text().notNull(), + description: text().notNull(), + category: text().notNull(), + amountRappen: integer().notNull(), + date: timestamp({ precision: 3, mode: 'date' }).notNull(), +}, (table) => [ + index("Expense_housingUnitId_date_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.date.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "Expense_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.paidById], + foreignColumns: [resident.id], + name: "Expense_paidById_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.createdById], + foreignColumns: [resident.id], + name: "Expense_createdById_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const expenseShare = pgTable("ExpenseShare", { + id: text().primaryKey().$defaultFn(createId).notNull(), + expenseId: text().notNull(), + residentId: text().notNull(), + amountRappen: integer().notNull(), +}, (table) => [ + uniqueIndex("ExpenseShare_expenseId_residentId_key").using("btree", table.expenseId.asc().nullsLast(), table.residentId.asc().nullsLast()), + index("ExpenseShare_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.expenseId], + foreignColumns: [expense.id], + name: "ExpenseShare_expenseId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "ExpenseShare_residentId_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const settlement = pgTable("Settlement", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + housingUnitId: text().notNull(), + fromId: text().notNull(), + toId: text().notNull(), + amountRappen: integer().notNull(), + note: text(), +}, (table) => [ + index("Settlement_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "Settlement_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.fromId], + foreignColumns: [resident.id], + name: "Settlement_fromId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.toId], + foreignColumns: [resident.id], + name: "Settlement_toId_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const account = pgTable("Account", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + email: text().notNull(), + passwordHash: text(), + emailVerifiedAt: timestamp({ precision: 3, mode: 'date' }), + userId: text(), + residentId: text(), +}, (table) => [ + uniqueIndex("Account_email_key").using("btree", table.email.asc().nullsLast()), + index("Account_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + uniqueIndex("Account_residentId_key").using("btree", table.residentId.asc().nullsLast()), + index("Account_userId_idx").using("btree", table.userId.asc().nullsLast()), + uniqueIndex("Account_userId_key").using("btree", table.userId.asc().nullsLast()), + foreignKey({ + columns: [table.userId], + foreignColumns: [user.id], + name: "Account_userId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "Account_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const authToken = pgTable("AuthToken", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + tokenHash: text().notNull(), + purpose: authTokenPurpose().notNull(), + expiresAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + usedAt: timestamp({ precision: 3, mode: 'date' }), + accountId: text().notNull(), +}, (table) => [ + index("AuthToken_accountId_purpose_idx").using("btree", table.accountId.asc().nullsLast(), table.purpose.asc().nullsLast()), + uniqueIndex("AuthToken_tokenHash_key").using("btree", table.tokenHash.asc().nullsLast()), + foreignKey({ + columns: [table.accountId], + foreignColumns: [account.id], + name: "AuthToken_accountId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const taskCompletion = pgTable("TaskCompletion", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + taskId: text().notNull(), + completedById: text().notNull(), + completedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + notes: text(), + durationMinutes: integer(), + completedItems: text().array().default(sql`ARRAY[]::TEXT[]`), +}, (table) => [ + index("TaskCompletion_completedById_idx").using("btree", table.completedById.asc().nullsLast()), + index("TaskCompletion_taskId_idx").using("btree", table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: "TaskCompletion_taskId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.completedById], + foreignColumns: [resident.id], + name: "TaskCompletion_completedById_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const learningRecord = pgTable("LearningRecord", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + kind: learningKind().notNull(), + title: text().notNull(), + status: learningStatus().default('PLANNED').notNull(), + languageCode: text(), + cefrLevel: text(), + provider: text(), + category: text(), + hours: integer(), + startedAt: timestamp({ precision: 3, mode: 'date' }), + completedAt: timestamp({ precision: 3, mode: 'date' }), + notes: text(), + recordedBy: residentOrStaff().notNull(), +}, (table) => [ + index("LearningRecord_languageCode_cefrLevel_idx").using("btree", table.languageCode.asc().nullsLast(), table.cefrLevel.asc().nullsLast()), + index("LearningRecord_residentId_kind_idx").using("btree", table.residentId.asc().nullsLast(), table.kind.asc().nullsLast()), + index("LearningRecord_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "LearningRecord_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const messageThread = pgTable("MessageThread", { + id: text().primaryKey().$defaultFn(createId).notNull(), + residentId: text().notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), +}, (table) => [ + uniqueIndex("MessageThread_residentId_key").using("btree", table.residentId.asc().nullsLast()), + index("MessageThread_updatedAt_idx").using("btree", table.updatedAt.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "MessageThread_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const message = pgTable("Message", { + id: text().primaryKey().$defaultFn(createId).notNull(), + threadId: text().notNull(), + authorResidentId: text(), + authorUserId: text(), + body: text().notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + readAt: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + index("Message_threadId_createdAt_idx").using("btree", table.threadId.asc().nullsLast(), table.createdAt.asc().nullsLast()), + foreignKey({ + columns: [table.threadId], + foreignColumns: [messageThread.id], + name: "Message_threadId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.authorResidentId], + foreignColumns: [resident.id], + name: "Message_authorResidentId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.authorUserId], + foreignColumns: [user.id], + name: "Message_authorUserId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + check("Message_one_author", sql`("authorResidentId" IS NOT NULL) <> ("authorUserId" IS NOT NULL)`), +]); + +export const careAssignment = pgTable("CareAssignment", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + staffId: text().notNull(), + role: careRole().notNull(), +}, (table) => [ + uniqueIndex("CareAssignment_residentId_role_key").using("btree", table.residentId.asc().nullsLast(), table.role.asc().nullsLast()), + index("CareAssignment_staffId_idx").using("btree", table.staffId.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "CareAssignment_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.staffId], + foreignColumns: [user.id], + name: "CareAssignment_staffId_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const careAttribute = pgTable("CareAttribute", { + id: text().primaryKey().$defaultFn(createId).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + domain: careRole().notNull(), + key: text().notNull(), + value: text().notNull(), + updatedById: text().notNull(), +}, (table) => [ + index("CareAttribute_residentId_domain_idx").using("btree", table.residentId.asc().nullsLast(), table.domain.asc().nullsLast()), + uniqueIndex("CareAttribute_residentId_domain_key_key").using("btree", table.residentId.asc().nullsLast(), table.domain.asc().nullsLast(), table.key.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "CareAttribute_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.updatedById], + foreignColumns: [user.id], + name: "CareAttribute_updatedById_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const houseEvent = pgTable("HouseEvent", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + title: text().notNull(), + description: text().notNull(), + category: houseEventCategory().default('SOCIAL').notNull(), + location: text(), + startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + endsAt: timestamp({ precision: 3, mode: 'date' }), + status: houseEventStatus().default('PUBLISHED').notNull(), + createdByStaffId: text(), + createdByResidentId: text(), +}, (table) => [ + index("HouseEvent_housingUnitId_startsAt_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.startsAt.asc().nullsLast()), + index("HouseEvent_status_startsAt_idx").using("btree", table.status.asc().nullsLast(), table.startsAt.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "HouseEvent_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.createdByStaffId], + foreignColumns: [user.id], + name: "HouseEvent_createdByStaffId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.createdByResidentId], + foreignColumns: [resident.id], + name: "HouseEvent_createdByResidentId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const appointment = pgTable("Appointment", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + staffId: text(), + domain: careRole().notNull(), + title: text().notNull(), + startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + endsAt: timestamp({ precision: 3, mode: 'date' }), + location: text(), + notes: text(), + status: appointmentStatus().default('SCHEDULED').notNull(), + residentNote: text(), + staffNote: text(), +}, (table) => [ + index("Appointment_residentId_startsAt_idx").using("btree", table.residentId.asc().nullsLast(), table.startsAt.asc().nullsLast()), + index("Appointment_staffId_startsAt_idx").using("btree", table.staffId.asc().nullsLast(), table.startsAt.asc().nullsLast()), + index("Appointment_status_domain_idx").using("btree", table.status.asc().nullsLast(), table.domain.asc().nullsLast()), + index("Appointment_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "Appointment_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.staffId], + foreignColumns: [user.id], + name: "Appointment_staffId_fkey" + }).onUpdate("cascade").onDelete("restrict"), +]); + +export const eventRsvp = pgTable("EventRsvp", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + eventId: text().notNull(), + residentId: text().notNull(), + status: eventRsvpStatus().default('GOING').notNull(), +}, (table) => [ + index("EventRsvp_eventId_idx").using("btree", table.eventId.asc().nullsLast()), + uniqueIndex("EventRsvp_eventId_residentId_key").using("btree", table.eventId.asc().nullsLast(), table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.eventId], + foreignColumns: [houseEvent.id], + name: "EventRsvp_eventId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "EventRsvp_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); + +export const opportunity = pgTable("Opportunity", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + kind: opportunityKind().notNull(), + title: text().notNull(), + description: text().notNull(), + organisation: text().notNull(), + location: text(), + schedule: text(), + hoursPerWeek: integer(), + seats: integer(), + germanLevel: text(), + permitRequirement: permitRequirement().default('NONE').notNull(), + requirementNote: text(), + contactName: text(), + contactEmail: text(), + contactPhone: text(), + website: text(), + status: opportunityStatus().default('DRAFT').notNull(), + startsAt: timestamp({ precision: 3, mode: 'date' }), + endsAt: timestamp({ precision: 3, mode: 'date' }), + createdByUserId: text(), + updatedByUserId: text(), +}, (table) => [ + index("Opportunity_endsAt_idx").using("btree", table.endsAt.asc().nullsLast()), + index("Opportunity_status_kind_idx").using("btree", table.status.asc().nullsLast(), table.kind.asc().nullsLast()), + foreignKey({ + columns: [table.createdByUserId], + foreignColumns: [user.id], + name: "Opportunity_createdByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.updatedByUserId], + foreignColumns: [user.id], + name: "Opportunity_updatedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const opportunityApplication = pgTable("OpportunityApplication", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + opportunityId: text().notNull(), + stage: applicationStage().default('INTERESTED').notNull(), + stageChangedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + note: text(), + createdBy: residentOrStaff().notNull(), + supportedByUserId: text(), + learningRecordId: text(), +}, (table) => [ + uniqueIndex("OpportunityApplication_learningRecordId_key").using("btree", table.learningRecordId.asc().nullsLast()), + index("OpportunityApplication_opportunityId_stage_idx").using("btree", table.opportunityId.asc().nullsLast(), table.stage.asc().nullsLast()), + index("OpportunityApplication_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + uniqueIndex("OpportunityApplication_residentId_opportunityId_key").using("btree", table.residentId.asc().nullsLast(), table.opportunityId.asc().nullsLast()), + index("OpportunityApplication_stage_idx").using("btree", table.stage.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "OpportunityApplication_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.opportunityId], + foreignColumns: [opportunity.id], + name: "OpportunityApplication_opportunityId_fkey" + }).onUpdate("cascade").onDelete("restrict"), + foreignKey({ + columns: [table.supportedByUserId], + foreignColumns: [user.id], + name: "OpportunityApplication_supportedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.learningRecordId], + foreignColumns: [learningRecord.id], + name: "OpportunityApplication_learningRecordId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const marketplacePost = pgTable("MarketplacePost", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + postedById: text().notNull(), + title: text().notNull(), + description: text().notNull(), + kind: marketplacePostKind().notNull(), + category: text().default('OTHER').notNull(), + status: marketplacePostStatus().default('OPEN').notNull(), + claimedById: text(), + closedAt: timestamp({ precision: 3, mode: 'date' }), + hiddenByStaff: boolean().default(false).notNull(), + hiddenReason: text(), + contactNote: text(), + claimedAt: timestamp({ precision: 3, mode: 'date' }), +}, (table) => [ + index("MarketplacePost_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), + index("MarketplacePost_postedById_idx").using("btree", table.postedById.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: "MarketplacePost_housingUnitId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.postedById], + foreignColumns: [resident.id], + name: "MarketplacePost_postedById_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.claimedById], + foreignColumns: [resident.id], + name: "MarketplacePost_claimedById_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const complaint = pgTable("Complaint", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text(), + subject: complaintSubject().notNull(), + body: text().notNull(), + status: complaintStatus().default('OPEN').notNull(), + response: text(), + respondedAt: timestamp({ precision: 3, mode: 'date' }), + respondedByUserId: text(), +}, (table) => [ + index("Complaint_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), + index("Complaint_residentId_idx").using("btree", table.residentId.asc().nullsLast()), + index("Complaint_status_idx").using("btree", table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "Complaint_residentId_fkey" + }).onUpdate("cascade").onDelete("set null"), + foreignKey({ + columns: [table.respondedByUserId], + foreignColumns: [user.id], + name: "Complaint_respondedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const residentDocument = pgTable("ResidentDocument", { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + category: text().default('OTHER').notNull(), + title: text().notNull(), + fileName: text().notNull(), + mimeType: text().notNull(), + sizeBytes: integer().notNull(), + uploadedByUserId: text(), +}, (table) => [ + index("ResidentDocument_residentId_createdAt_idx").using("btree", table.residentId.asc().nullsLast(), table.createdAt.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: "ResidentDocument_residentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), + foreignKey({ + columns: [table.uploadedByUserId], + foreignColumns: [user.id], + name: "ResidentDocument_uploadedByUserId_fkey" + }).onUpdate("cascade").onDelete("set null"), +]); + +export const residentDocumentBlob = pgTable("ResidentDocumentBlob", { + documentId: text().primaryKey().notNull(), + data: bytea().notNull(), +}, (table) => [ + foreignKey({ + columns: [table.documentId], + foreignColumns: [residentDocument.id], + name: "ResidentDocumentBlob_documentId_fkey" + }).onUpdate("cascade").onDelete("cascade"), +]); diff --git a/src/lib/db/types.ts b/src/lib/db/types.ts new file mode 100644 index 00000000..f66e9b08 --- /dev/null +++ b/src/lib/db/types.ts @@ -0,0 +1,683 @@ +/** + * Derived types + runtime enum objects — the drop-in replacement for what + * `@prisma/client` used to export. + * + * Prisma generated, for every enum, BOTH a type and a same-named runtime + * object (`StaffRole.ADMIN`). Call sites use the values, so the objects are + * reproduced here, derived from the pgEnum declarations in schema.ts — one + * source of truth, two views of it. + */ +import type * as s from './schema' + +// --------------------------------------------------------------------------- +// Row types (SELECT shape — what Prisma's model types were) +// --------------------------------------------------------------------------- +export type AlgorithmWeight = typeof s.algorithmWeight.$inferSelect +export type PlacementSpot = typeof s.placementSpot.$inferSelect +export type Placement = typeof s.placement.$inferSelect +export type CompatibilityAssessment = typeof s.compatibilityAssessment.$inferSelect +export type IncidentFollowUp = typeof s.incidentFollowUp.$inferSelect +export type IncidentInvolvement = typeof s.incidentInvolvement.$inferSelect +export type AuditLog = typeof s.auditLog.$inferSelect +export type MaintenanceRequest = typeof s.maintenanceRequest.$inferSelect +export type TaskAttentionFlag = typeof s.taskAttentionFlag.$inferSelect +export type HousingUnit = typeof s.housingUnit.$inferSelect +export type HouseholdTask = typeof s.householdTask.$inferSelect +export type SatisfactionCheckIn = typeof s.satisfactionCheckIn.$inferSelect +export type User = typeof s.user.$inferSelect +export type TaskRequest = typeof s.taskRequest.$inferSelect +export type TransferRequest = typeof s.transferRequest.$inferSelect +export type Resident = typeof s.resident.$inferSelect +export type Activity = typeof s.activity.$inferSelect +export type SystemConfig = typeof s.systemConfig.$inferSelect +export type Incident = typeof s.incident.$inferSelect +export type HouseRule = typeof s.houseRule.$inferSelect +export type Proposal = typeof s.proposal.$inferSelect +export type RuleAcknowledgement = typeof s.ruleAcknowledgement.$inferSelect +export type Vote = typeof s.vote.$inferSelect +export type ConflictAgreement = typeof s.conflictAgreement.$inferSelect +export type AgreementParty = typeof s.agreementParty.$inferSelect +export type ResidentPhoto = typeof s.residentPhoto.$inferSelect +export type Expense = typeof s.expense.$inferSelect +export type ExpenseShare = typeof s.expenseShare.$inferSelect +export type Settlement = typeof s.settlement.$inferSelect +export type Account = typeof s.account.$inferSelect +export type AuthToken = typeof s.authToken.$inferSelect +export type TaskCompletion = typeof s.taskCompletion.$inferSelect +export type LearningRecord = typeof s.learningRecord.$inferSelect +export type MessageThread = typeof s.messageThread.$inferSelect +export type Message = typeof s.message.$inferSelect +export type CareAssignment = typeof s.careAssignment.$inferSelect +export type CareAttribute = typeof s.careAttribute.$inferSelect +export type HouseEvent = typeof s.houseEvent.$inferSelect +export type Appointment = typeof s.appointment.$inferSelect +export type EventRsvp = typeof s.eventRsvp.$inferSelect +export type Opportunity = typeof s.opportunity.$inferSelect +export type OpportunityApplication = typeof s.opportunityApplication.$inferSelect +export type MarketplacePost = typeof s.marketplacePost.$inferSelect +export type Complaint = typeof s.complaint.$inferSelect +export type ResidentDocument = typeof s.residentDocument.$inferSelect +export type ResidentDocumentBlob = typeof s.residentDocumentBlob.$inferSelect + +// --------------------------------------------------------------------------- +// Insert shapes (what Prisma's ...CreateInput approximated) +// --------------------------------------------------------------------------- +export type NewAlgorithmWeight = typeof s.algorithmWeight.$inferInsert +export type NewPlacementSpot = typeof s.placementSpot.$inferInsert +export type NewPlacement = typeof s.placement.$inferInsert +export type NewCompatibilityAssessment = typeof s.compatibilityAssessment.$inferInsert +export type NewIncidentFollowUp = typeof s.incidentFollowUp.$inferInsert +export type NewIncidentInvolvement = typeof s.incidentInvolvement.$inferInsert +export type NewAuditLog = typeof s.auditLog.$inferInsert +export type NewMaintenanceRequest = typeof s.maintenanceRequest.$inferInsert +export type NewTaskAttentionFlag = typeof s.taskAttentionFlag.$inferInsert +export type NewHousingUnit = typeof s.housingUnit.$inferInsert +export type NewHouseholdTask = typeof s.householdTask.$inferInsert +export type NewSatisfactionCheckIn = typeof s.satisfactionCheckIn.$inferInsert +export type NewUser = typeof s.user.$inferInsert +export type NewTaskRequest = typeof s.taskRequest.$inferInsert +export type NewTransferRequest = typeof s.transferRequest.$inferInsert +export type NewResident = typeof s.resident.$inferInsert +export type NewActivity = typeof s.activity.$inferInsert +export type NewSystemConfig = typeof s.systemConfig.$inferInsert +export type NewIncident = typeof s.incident.$inferInsert +export type NewHouseRule = typeof s.houseRule.$inferInsert +export type NewProposal = typeof s.proposal.$inferInsert +export type NewRuleAcknowledgement = typeof s.ruleAcknowledgement.$inferInsert +export type NewVote = typeof s.vote.$inferInsert +export type NewConflictAgreement = typeof s.conflictAgreement.$inferInsert +export type NewAgreementParty = typeof s.agreementParty.$inferInsert +export type NewResidentPhoto = typeof s.residentPhoto.$inferInsert +export type NewExpense = typeof s.expense.$inferInsert +export type NewExpenseShare = typeof s.expenseShare.$inferInsert +export type NewSettlement = typeof s.settlement.$inferInsert +export type NewAccount = typeof s.account.$inferInsert +export type NewAuthToken = typeof s.authToken.$inferInsert +export type NewTaskCompletion = typeof s.taskCompletion.$inferInsert +export type NewLearningRecord = typeof s.learningRecord.$inferInsert +export type NewMessageThread = typeof s.messageThread.$inferInsert +export type NewMessage = typeof s.message.$inferInsert +export type NewCareAssignment = typeof s.careAssignment.$inferInsert +export type NewCareAttribute = typeof s.careAttribute.$inferInsert +export type NewHouseEvent = typeof s.houseEvent.$inferInsert +export type NewAppointment = typeof s.appointment.$inferInsert +export type NewEventRsvp = typeof s.eventRsvp.$inferInsert +export type NewOpportunity = typeof s.opportunity.$inferInsert +export type NewOpportunityApplication = typeof s.opportunityApplication.$inferInsert +export type NewMarketplacePost = typeof s.marketplacePost.$inferInsert +export type NewComplaint = typeof s.complaint.$inferInsert +export type NewResidentDocument = typeof s.residentDocument.$inferInsert +export type NewResidentDocumentBlob = typeof s.residentDocumentBlob.$inferInsert + +// --------------------------------------------------------------------------- +// Enums: value objects + union types, Prisma-style +// --------------------------------------------------------------------------- +export const ActivityCategory = Object.freeze({ + SPORT: 'SPORT', + LANGUAGE: 'LANGUAGE', + CULTURE: 'CULTURE', + COMMUNITY: 'COMMUNITY', + FAMILY: 'FAMILY', + SUPPORT: 'SUPPORT', +} as const) satisfies Record +export type ActivityCategory = (typeof ActivityCategory)[keyof typeof ActivityCategory] + +export const ActivityCost = Object.freeze({ + FREE: 'FREE', + REDUCED: 'REDUCED', + PAID: 'PAID', +} as const) satisfies Record +export type ActivityCost = (typeof ActivityCost)[keyof typeof ActivityCost] + +export const ActivityStatus = Object.freeze({ + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + ARCHIVED: 'ARCHIVED', +} as const) satisfies Record +export type ActivityStatus = (typeof ActivityStatus)[keyof typeof ActivityStatus] + +export const AgeRange = Object.freeze({ + YOUNG_ADULT: 'YOUNG_ADULT', + ADULT: 'ADULT', + MIDDLE_AGED: 'MIDDLE_AGED', + SENIOR: 'SENIOR', +} as const) satisfies Record +export type AgeRange = (typeof AgeRange)[keyof typeof AgeRange] + +export const AgreementStatus = Object.freeze({ + PROPOSED: 'PROPOSED', + ACCEPTED: 'ACCEPTED', + HELD: 'HELD', + BROKEN: 'BROKEN', + EXPIRED: 'EXPIRED', +} as const) satisfies Record +export type AgreementStatus = (typeof AgreementStatus)[keyof typeof AgreementStatus] + +export const ApplicationStage = Object.freeze({ + INTERESTED: 'INTERESTED', + APPLIED: 'APPLIED', + INTERVIEW: 'INTERVIEW', + ACCEPTED: 'ACCEPTED', + STARTED: 'STARTED', + ENDED: 'ENDED', + DECLINED: 'DECLINED', +} as const) satisfies Record +export type ApplicationStage = (typeof ApplicationStage)[keyof typeof ApplicationStage] + +export const AppointmentStatus = Object.freeze({ + SCHEDULED: 'SCHEDULED', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', + NO_SHOW: 'NO_SHOW', + REQUESTED: 'REQUESTED', +} as const) satisfies Record +export type AppointmentStatus = (typeof AppointmentStatus)[keyof typeof AppointmentStatus] + +export const AuthTokenPurpose = Object.freeze({ + VERIFY_EMAIL: 'VERIFY_EMAIL', + RESET_PASSWORD: 'RESET_PASSWORD', +} as const) satisfies Record +export type AuthTokenPurpose = (typeof AuthTokenPurpose)[keyof typeof AuthTokenPurpose] + +export const CareRole = Object.freeze({ + HOUSING: 'HOUSING', + SOCIAL: 'SOCIAL', + JOB: 'JOB', + VOLUNTEERING: 'VOLUNTEERING', +} as const) satisfies Record +export type CareRole = (typeof CareRole)[keyof typeof CareRole] + +export const CheckInType = Object.freeze({ + INITIAL: 'INITIAL', + REGULAR: 'REGULAR', + AD_HOC: 'AD_HOC', + EXIT: 'EXIT', +} as const) satisfies Record +export type CheckInType = (typeof CheckInType)[keyof typeof CheckInType] + +export const ComplaintStatus = Object.freeze({ + OPEN: 'OPEN', + IN_REVIEW: 'IN_REVIEW', + ANSWERED: 'ANSWERED', +} as const) satisfies Record +export type ComplaintStatus = (typeof ComplaintStatus)[keyof typeof ComplaintStatus] + +export const ComplaintSubject = Object.freeze({ + STAFF: 'STAFF', + ACCOMMODATION: 'ACCOMMODATION', + DECISION: 'DECISION', + OTHER: 'OTHER', +} as const) satisfies Record +export type ComplaintSubject = (typeof ComplaintSubject)[keyof typeof ComplaintSubject] + +export const ConflictStyle = Object.freeze({ + AVOIDANT: 'AVOIDANT', + COOPERATIVE: 'COOPERATIVE', + DIRECT: 'DIRECT', +} as const) satisfies Record +export type ConflictStyle = (typeof ConflictStyle)[keyof typeof ConflictStyle] + +export const DecisionMode = Object.freeze({ + RESIDENT_BINDING: 'RESIDENT_BINDING', + RESIDENT_ADVISORY: 'RESIDENT_ADVISORY', + STAFF_ONLY: 'STAFF_ONLY', +} as const) satisfies Record +export type DecisionMode = (typeof DecisionMode)[keyof typeof DecisionMode] + +export const EndReason = Object.freeze({ + NATURAL: 'NATURAL', + CONFLICT: 'CONFLICT', + REQUEST: 'REQUEST', + CAPACITY: 'CAPACITY', + UPGRADE: 'UPGRADE', + OTHER: 'OTHER', +} as const) satisfies Record +export type EndReason = (typeof EndReason)[keyof typeof EndReason] + +export const EventRsvpStatus = Object.freeze({ + GOING: 'GOING', + MAYBE: 'MAYBE', + DECLINED: 'DECLINED', +} as const) satisfies Record +export type EventRsvpStatus = (typeof EventRsvpStatus)[keyof typeof EventRsvpStatus] + +export const FamilyStatus = Object.freeze({ + SINGLE: 'SINGLE', + COUPLE: 'COUPLE', + FAMILY_WITH_CHILDREN: 'FAMILY_WITH_CHILDREN', + SINGLE_PARENT: 'SINGLE_PARENT', +} as const) satisfies Record +export type FamilyStatus = (typeof FamilyStatus)[keyof typeof FamilyStatus] + +export const FollowUpPriority = Object.freeze({ + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', +} as const) satisfies Record +export type FollowUpPriority = (typeof FollowUpPriority)[keyof typeof FollowUpPriority] + +export const Gender = Object.freeze({ + MALE: 'MALE', + FEMALE: 'FEMALE', + OTHER: 'OTHER', + PREFER_NOT_SAY: 'PREFER_NOT_SAY', +} as const) satisfies Record +export type Gender = (typeof Gender)[keyof typeof Gender] + +export const HouseEventCategory = Object.freeze({ + HOUSE_MEETING: 'HOUSE_MEETING', + SOCIAL: 'SOCIAL', + CULTURE: 'CULTURE', + SUPPORT: 'SUPPORT', +} as const) satisfies Record +export type HouseEventCategory = (typeof HouseEventCategory)[keyof typeof HouseEventCategory] + +export const HouseEventStatus = Object.freeze({ + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + CANCELLED: 'CANCELLED', +} as const) satisfies Record +export type HouseEventStatus = (typeof HouseEventStatus)[keyof typeof HouseEventStatus] + +export const HouseholdTaskCategory = Object.freeze({ + CLEANING: 'CLEANING', + SHOPPING: 'SHOPPING', + MAINTENANCE: 'MAINTENANCE', + COOKING: 'COOKING', + TRASH: 'TRASH', + OTHER: 'OTHER', +} as const) satisfies Record +export type HouseholdTaskCategory = (typeof HouseholdTaskCategory)[keyof typeof HouseholdTaskCategory] + +export const HouseholdTaskPriority = Object.freeze({ + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', +} as const) satisfies Record +export type HouseholdTaskPriority = (typeof HouseholdTaskPriority)[keyof typeof HouseholdTaskPriority] + +export const HouseholdTaskStatus = Object.freeze({ + IDLE: 'IDLE', + NEEDS_ATTENTION: 'NEEDS_ATTENTION', + REQUESTED: 'REQUESTED', + IN_PROGRESS: 'IN_PROGRESS', +} as const) satisfies Record +export type HouseholdTaskStatus = (typeof HouseholdTaskStatus)[keyof typeof HouseholdTaskStatus] + +export const HouseholdTaskType = Object.freeze({ + ONE_TIME: 'ONE_TIME', + RECURRING_SCHEDULED: 'RECURRING_SCHEDULED', + RECURRING_AS_NEEDED: 'RECURRING_AS_NEEDED', +} as const) satisfies Record +export type HouseholdTaskType = (typeof HouseholdTaskType)[keyof typeof HouseholdTaskType] + +export const HousingStatus = Object.freeze({ + AVAILABLE: 'AVAILABLE', + FULL: 'FULL', + MAINTENANCE: 'MAINTENANCE', + CLOSED: 'CLOSED', +} as const) satisfies Record +export type HousingStatus = (typeof HousingStatus)[keyof typeof HousingStatus] + +export const IncidentCategory = Object.freeze({ + INTERPERSONAL: 'INTERPERSONAL', + MAINTENANCE: 'MAINTENANCE', + SAFETY: 'SAFETY', + WELLBEING: 'WELLBEING', +} as const) satisfies Record +export type IncidentCategory = (typeof IncidentCategory)[keyof typeof IncidentCategory] + +export const IncidentSeverity = Object.freeze({ + LOW: 'LOW', + MEDIUM: 'MEDIUM', + HIGH: 'HIGH', + CRITICAL: 'CRITICAL', +} as const) satisfies Record +export type IncidentSeverity = (typeof IncidentSeverity)[keyof typeof IncidentSeverity] + +export const IncidentType = Object.freeze({ + NOISE_COMPLAINT: 'NOISE_COMPLAINT', + CLEANLINESS_DISPUTE: 'CLEANLINESS_DISPUTE', + PERSONAL_CONFLICT: 'PERSONAL_CONFLICT', + CULTURAL_FRICTION: 'CULTURAL_FRICTION', + SPACE_DISPUTE: 'SPACE_DISPUTE', + SCHEDULE_CONFLICT: 'SCHEDULE_CONFLICT', + SAFETY_CONCERN: 'SAFETY_CONCERN', + PLUMBING: 'PLUMBING', + ELECTRICAL: 'ELECTRICAL', + HEATING_COOLING: 'HEATING_COOLING', + APPLIANCE: 'APPLIANCE', + STRUCTURAL: 'STRUCTURAL', + PEST_CONTROL: 'PEST_CONTROL', + SECURITY_SYSTEM: 'SECURITY_SYSTEM', + GENERAL_MAINTENANCE: 'GENERAL_MAINTENANCE', + LOW_SATISFACTION: 'LOW_SATISFACTION', + OTHER: 'OTHER', +} as const) satisfies Record +export type IncidentType = (typeof IncidentType)[keyof typeof IncidentType] + +export const InvolvementRole = Object.freeze({ + INVOLVED: 'INVOLVED', + WITNESS: 'WITNESS', + MEDIATOR: 'MEDIATOR', +} as const) satisfies Record +export type InvolvementRole = (typeof InvolvementRole)[keyof typeof InvolvementRole] + +export const LearningKind = Object.freeze({ + LANGUAGE_TEST: 'LANGUAGE_TEST', + COURSE: 'COURSE', + INFORMAL: 'INFORMAL', + QUALIFICATION: 'QUALIFICATION', + VOLUNTEERING: 'VOLUNTEERING', + COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', + EMPLOYMENT: 'EMPLOYMENT', + INTERNSHIP: 'INTERNSHIP', +} as const) satisfies Record +export type LearningKind = (typeof LearningKind)[keyof typeof LearningKind] + +export const LearningStatus = Object.freeze({ + PLANNED: 'PLANNED', + IN_PROGRESS: 'IN_PROGRESS', + COMPLETED: 'COMPLETED', + EXPIRED: 'EXPIRED', +} as const) satisfies Record +export type LearningStatus = (typeof LearningStatus)[keyof typeof LearningStatus] + +export const LivingSkillsSupport = Object.freeze({ + INDEPENDENT: 'INDEPENDENT', + SOME_SUPPORT: 'SOME_SUPPORT', + REGULAR_SUPPORT: 'REGULAR_SUPPORT', +} as const) satisfies Record +export type LivingSkillsSupport = (typeof LivingSkillsSupport)[keyof typeof LivingSkillsSupport] + +export const MaintenanceCategory = Object.freeze({ + PLUMBING: 'PLUMBING', + ELECTRICAL: 'ELECTRICAL', + HEATING_COOLING: 'HEATING_COOLING', + APPLIANCE: 'APPLIANCE', + STRUCTURAL: 'STRUCTURAL', + PEST_CONTROL: 'PEST_CONTROL', + SECURITY: 'SECURITY', + CLEANING: 'CLEANING', + EXTERIOR: 'EXTERIOR', + OTHER: 'OTHER', +} as const) satisfies Record +export type MaintenanceCategory = (typeof MaintenanceCategory)[keyof typeof MaintenanceCategory] + +export const MaintenancePriority = Object.freeze({ + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', +} as const) satisfies Record +export type MaintenancePriority = (typeof MaintenancePriority)[keyof typeof MaintenancePriority] + +export const MaintenanceStatus = Object.freeze({ + OPEN: 'OPEN', + ASSIGNED: 'ASSIGNED', + IN_PROGRESS: 'IN_PROGRESS', + ON_HOLD: 'ON_HOLD', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', +} as const) satisfies Record +export type MaintenanceStatus = (typeof MaintenanceStatus)[keyof typeof MaintenanceStatus] + +export const MarketplacePostKind = Object.freeze({ + GIVE_AWAY: 'GIVE_AWAY', + LEND: 'LEND', + WANTED: 'WANTED', + OFFER_HELP: 'OFFER_HELP', + NEED_HELP: 'NEED_HELP', +} as const) satisfies Record +export type MarketplacePostKind = (typeof MarketplacePostKind)[keyof typeof MarketplacePostKind] + +export const MarketplacePostStatus = Object.freeze({ + OPEN: 'OPEN', + CLAIMED: 'CLAIMED', + CLOSED: 'CLOSED', +} as const) satisfies Record +export type MarketplacePostStatus = (typeof MarketplacePostStatus)[keyof typeof MarketplacePostStatus] + +export const MedicalDocType = Object.freeze({ + PRIVATE_ROOM: 'PRIVATE_ROOM', + STUDIO: 'STUDIO', + BOTH: 'BOTH', +} as const) satisfies Record +export type MedicalDocType = (typeof MedicalDocType)[keyof typeof MedicalDocType] + +export const MobilityNeed = Object.freeze({ + NONE: 'NONE', + GROUND_FLOOR: 'GROUND_FLOOR', + WHEELCHAIR: 'WHEELCHAIR', +} as const) satisfies Record +export type MobilityNeed = (typeof MobilityNeed)[keyof typeof MobilityNeed] + +export const OpportunityKind = Object.freeze({ + VOLUNTEERING: 'VOLUNTEERING', + COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', + EMPLOYMENT: 'EMPLOYMENT', + INTERNSHIP: 'INTERNSHIP', +} as const) satisfies Record +export type OpportunityKind = (typeof OpportunityKind)[keyof typeof OpportunityKind] + +export const OpportunityStatus = Object.freeze({ + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + ARCHIVED: 'ARCHIVED', +} as const) satisfies Record +export type OpportunityStatus = (typeof OpportunityStatus)[keyof typeof OpportunityStatus] + +export const PermitRequirement = Object.freeze({ + NONE: 'NONE', + EMPLOYER_NOTIFIES: 'EMPLOYER_NOTIFIES', + PERMIT_REQUIRED: 'PERMIT_REQUIRED', +} as const) satisfies Record +export type PermitRequirement = (typeof PermitRequirement)[keyof typeof PermitRequirement] + +export const PlacementStatus = Object.freeze({ + ACTIVE: 'ACTIVE', + ENDED: 'ENDED', + TRANSFERRED: 'TRANSFERRED', +} as const) satisfies Record +export type PlacementStatus = (typeof PlacementStatus)[keyof typeof PlacementStatus] + +export const ProfileVisibility = Object.freeze({ + PRIVATE: 'PRIVATE', + ROOMMATES: 'ROOMMATES', + RESIDENTS: 'RESIDENTS', +} as const) satisfies Record +export type ProfileVisibility = (typeof ProfileVisibility)[keyof typeof ProfileVisibility] + +export const ProposalStatus = Object.freeze({ + DISCUSSION: 'DISCUSSION', + VOTING: 'VOTING', + NEEDS_STAFF_CONFIRMATION: 'NEEDS_STAFF_CONFIRMATION', + ACCEPTED: 'ACCEPTED', + REJECTED: 'REJECTED', + WITHDRAWN: 'WITHDRAWN', + VETOED: 'VETOED', + EXPIRED: 'EXPIRED', +} as const) satisfies Record +export type ProposalStatus = (typeof ProposalStatus)[keyof typeof ProposalStatus] + +export const ProposalType = Object.freeze({ + ADD_RULE: 'ADD_RULE', + AMEND_RULE: 'AMEND_RULE', + REPEAL_RULE: 'REPEAL_RULE', + HOUSE_DECISION: 'HOUSE_DECISION', +} as const) satisfies Record +export type ProposalType = (typeof ProposalType)[keyof typeof ProposalType] + +export const RecyclingKnowledge = Object.freeze({ + NONE: 'NONE', + BASIC: 'BASIC', + GOOD: 'GOOD', +} as const) satisfies Record +export type RecyclingKnowledge = (typeof RecyclingKnowledge)[keyof typeof RecyclingKnowledge] + +export const ResidentOrStaff = Object.freeze({ + RESIDENT: 'RESIDENT', + STAFF: 'STAFF', +} as const) satisfies Record +export type ResidentOrStaff = (typeof ResidentOrStaff)[keyof typeof ResidentOrStaff] + +export const ResidentStatus = Object.freeze({ + ACTIVE: 'ACTIVE', + PLACED: 'PLACED', + TRANSFERRED: 'TRANSFERRED', + EXITED: 'EXITED', +} as const) satisfies Record +export type ResidentStatus = (typeof ResidentStatus)[keyof typeof ResidentStatus] + +export const ResolutionStage = Object.freeze({ + REPORTED: 'REPORTED', + SELF_RESOLUTION: 'SELF_RESOLUTION', + PEER_MEDIATION: 'PEER_MEDIATION', + STAFF_MEDIATION: 'STAFF_MEDIATION', + FORMAL_MEASURE: 'FORMAL_MEASURE', + CLOSED: 'CLOSED', +} as const) satisfies Record +export type ResolutionStage = (typeof ResolutionStage)[keyof typeof ResolutionStage] + +export const RoomSharingStatus = Object.freeze({ + CAN_SHARE: 'CAN_SHARE', + PREFERS_PRIVATE: 'PREFERS_PRIVATE', + NEEDS_PRIVATE: 'NEEDS_PRIVATE', +} as const) satisfies Record +export type RoomSharingStatus = (typeof RoomSharingStatus)[keyof typeof RoomSharingStatus] + +export const RuleCategory = Object.freeze({ + SAFETY: 'SAFETY', + RESPECT: 'RESPECT', + NOISE: 'NOISE', + CLEANLINESS: 'CLEANLINESS', + KITCHEN: 'KITCHEN', + BATHROOM: 'BATHROOM', + GUESTS: 'GUESTS', + SHARED_SPACES: 'SHARED_SPACES', + COSTS: 'COSTS', + COMMUNICATION: 'COMMUNICATION', + OTHER: 'OTHER', +} as const) satisfies Record +export type RuleCategory = (typeof RuleCategory)[keyof typeof RuleCategory] + +export const RuleDelegation = Object.freeze({ + FIXED: 'FIXED', + UNIT_MAY_STRENGTHEN: 'UNIT_MAY_STRENGTHEN', + UNIT_DECIDES: 'UNIT_DECIDES', +} as const) satisfies Record +export type RuleDelegation = (typeof RuleDelegation)[keyof typeof RuleDelegation] + +export const RuleScope = Object.freeze({ + ORG: 'ORG', + UNIT: 'UNIT', +} as const) satisfies Record +export type RuleScope = (typeof RuleScope)[keyof typeof RuleScope] + +export const RuleStatus = Object.freeze({ + ACTIVE: 'ACTIVE', + SUPERSEDED: 'SUPERSEDED', + ARCHIVED: 'ARCHIVED', +} as const) satisfies Record +export type RuleStatus = (typeof RuleStatus)[keyof typeof RuleStatus] + +export const SleepSchedule = Object.freeze({ + EARLY_BIRD: 'EARLY_BIRD', + STANDARD: 'STANDARD', + NIGHT_OWL: 'NIGHT_OWL', + IRREGULAR: 'IRREGULAR', +} as const) satisfies Record +export type SleepSchedule = (typeof SleepSchedule)[keyof typeof SleepSchedule] + +export const SmokingStatus = Object.freeze({ + NON_SMOKER: 'NON_SMOKER', + OUTDOOR_SMOKER: 'OUTDOOR_SMOKER', + INDOOR_SMOKER: 'INDOOR_SMOKER', +} as const) satisfies Record +export type SmokingStatus = (typeof SmokingStatus)[keyof typeof SmokingStatus] + +export const SocialStyle = Object.freeze({ + INTROVERTED: 'INTROVERTED', + MODERATE: 'MODERATE', + EXTROVERTED: 'EXTROVERTED', +} as const) satisfies Record +export type SocialStyle = (typeof SocialStyle)[keyof typeof SocialStyle] + +export const SpotStatus = Object.freeze({ + AVAILABLE: 'AVAILABLE', + OCCUPIED: 'OCCUPIED', + MAINTENANCE: 'MAINTENANCE', + CLOSED: 'CLOSED', +} as const) satisfies Record +export type SpotStatus = (typeof SpotStatus)[keyof typeof SpotStatus] + +export const SpotType = Object.freeze({ + BED: 'BED', + PRIVATE_ROOM: 'PRIVATE_ROOM', + STUDIO: 'STUDIO', + ROOM: 'ROOM', +} as const) satisfies Record +export type SpotType = (typeof SpotType)[keyof typeof SpotType] + +export const StaffDecision = Object.freeze({ + CONFIRMED: 'CONFIRMED', + VETOED: 'VETOED', +} as const) satisfies Record +export type StaffDecision = (typeof StaffDecision)[keyof typeof StaffDecision] + +export const StaffRole = Object.freeze({ + ADMIN: 'ADMIN', + BETREUUNG: 'BETREUUNG', + SOZIALARBEIT: 'SOZIALARBEIT', + JOBCOACH: 'JOBCOACH', + FREIWILLIGENARBEIT: 'FREIWILLIGENARBEIT', +} as const) satisfies Record +export type StaffRole = (typeof StaffRole)[keyof typeof StaffRole] + +export const StaffScope = Object.freeze({ + OWN_DOMAIN: 'OWN_DOMAIN', + ALL_DOMAINS: 'ALL_DOMAINS', +} as const) satisfies Record +export type StaffScope = (typeof StaffScope)[keyof typeof StaffScope] + +export const SupportLevel = Object.freeze({ + STANDARD: 'STANDARD', + ELEVATED: 'ELEVATED', + INTENSIVE: 'INTENSIVE', +} as const) satisfies Record +export type SupportLevel = (typeof SupportLevel)[keyof typeof SupportLevel] + +export const TaskRequestStatus = Object.freeze({ + PENDING: 'PENDING', + ACCEPTED: 'ACCEPTED', + DECLINED: 'DECLINED', + COMPLETED: 'COMPLETED', +} as const) satisfies Record +export type TaskRequestStatus = (typeof TaskRequestStatus)[keyof typeof TaskRequestStatus] + +export const TransferRequestStatus = Object.freeze({ + PENDING: 'PENDING', + APPROVED: 'APPROVED', + DENIED: 'DENIED', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', +} as const) satisfies Record +export type TransferRequestStatus = (typeof TransferRequestStatus)[keyof typeof TransferRequestStatus] + +export const VoteChoice = Object.freeze({ + YES: 'YES', + NO: 'NO', + ABSTAIN: 'ABSTAIN', + BLOCK: 'BLOCK', +} as const) satisfies Record +export type VoteChoice = (typeof VoteChoice)[keyof typeof VoteChoice] + +export const VoteThreshold = Object.freeze({ + CONSENSUS: 'CONSENSUS', + SUPERMAJORITY: 'SUPERMAJORITY', + SIMPLE_MAJORITY: 'SIMPLE_MAJORITY', +} as const) satisfies Record +export type VoteThreshold = (typeof VoteThreshold)[keyof typeof VoteThreshold] + From 95f8ae7c95f5c37e2e7fdf3e02c4a6c24ea24b3f Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:37:40 +0200 Subject: [PATCH 2/5] refactor(db): rewrite every Prisma call site to Drizzle (src + scripts + CI + docs) 116 src files with live call sites and 50 type-import files converted: prisma.x.find* -> db.query.x.*, create/update/delete -> insert/update/delete builders, upserts -> onConflictDoUpdate, $transaction -> db.transaction, $queryRaw -> db.execute(sql), Prisma.sql/join/empty -> drizzle sql, P2002 catches -> isUniqueViolation (SQLSTATE 23505, DrizzleQueryError-aware). Nullable TEXT[] columns (DB truth Prisma's types papered over) are normalised at the UI boundary. Type-level FK cycles (Placement<->Incident, HouseRule<->Proposal) broken with PgTableExtraConfigValue[] return annotations. CI: prisma generate steps dropped, schema init = npm run db:migrate, seeds = db:seed/db:seed:admin. deploy-demo.sh migrates via ledgered psql loop (same _deploy_schema_history the fleet applier keeps). Docs updated. Tests and prisma/seed*.ts still reference the old client - next commits. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn --- .env.example | 4 +- .github/workflows/ci.yml | 21 +- CLAUDE.md | 46 +- DEMO_GUIDE.md | 4 +- IMPLEMENTATION_SUMMARY.md | 10 +- README.md | 10 +- docs/INFRASTRUCTURE.md | 12 +- docs/ROLE-WALKTHROUGH-2026-08-31.md | 2 +- package.json | 18 +- scripts/calculate-compatibility.ts | 97 +- scripts/cleanup-test-data.ts | 57 +- scripts/create-witikon-residents.ts | 49 +- scripts/deploy-demo.sh | 27 +- scripts/maintenance/ensure-aoz-team.ts | 28 +- scripts/maintenance/ensure-operator.ts | 79 +- scripts/seed-witikon.ts | 64 +- scripts/test-troublemaker-detection.ts | 83 +- scripts/test-workflow.ts | 218 +- scripts/verify-data.ts | 35 +- src/app/(admin)/analytics/page.tsx | 61 +- src/app/(admin)/chores/new/page.tsx | 11 +- src/app/(admin)/chores/page.tsx | 29 +- src/app/(admin)/complaints/page.tsx | 15 +- src/app/(admin)/events/page.tsx | 8 +- src/app/(admin)/housing/[id]/edit/page.tsx | 7 +- src/app/(admin)/housing/[id]/page.tsx | 84 +- src/app/(admin)/housing/[id]/spots/page.tsx | 23 +- src/app/(admin)/housing/page.tsx | 93 +- src/app/(admin)/incidents/[id]/page.tsx | 31 +- src/app/(admin)/incidents/new/page.tsx | 15 +- src/app/(admin)/incidents/page.tsx | 81 +- src/app/(admin)/maintenance/[id]/page.tsx | 17 +- src/app/(admin)/maintenance/new/page.tsx | 23 +- src/app/(admin)/maintenance/page.tsx | 52 +- src/app/(admin)/matching/page.tsx | 88 +- .../(admin)/messages/[residentId]/page.tsx | 9 +- src/app/(admin)/page.tsx | 118 +- .../(admin)/placements/[id]/checkin/page.tsx | 13 +- src/app/(admin)/placements/page.tsx | 59 +- src/app/(admin)/residents/[id]/edit/page.tsx | 7 +- src/app/(admin)/residents/[id]/page.tsx | 106 +- src/app/(admin)/residents/page.tsx | 170 +- src/app/(admin)/settings/page.tsx | 15 +- src/app/api/auth/demo/route.ts | 17 +- src/app/api/auth/invite/route.ts | 74 +- src/app/api/auth/register/route.ts | 27 +- src/app/api/chores/route.ts | 18 +- src/app/api/cron/notifications/route.ts | 42 +- src/app/api/cron/reset-demo/route.ts | 12 +- src/app/api/export/incidents/route.ts | 7 +- src/app/api/export/placements/route.ts | 7 +- src/app/api/export/residents/route.ts | 7 +- src/app/api/export/satisfaction/route.ts | 7 +- src/app/api/health/route.ts | 5 +- src/app/api/import/residents/route.ts | 82 +- src/app/api/portal/apartment/route.ts | 13 +- .../api/portal/chores/[id]/attention/route.ts | 27 +- .../api/portal/chores/[id]/complaint/route.ts | 25 +- .../api/portal/chores/[id]/complete/route.ts | 58 +- .../api/portal/chores/[id]/request/route.ts | 43 +- src/app/api/portal/chores/[id]/route.ts | 59 +- src/app/api/portal/chores/route.ts | 36 +- src/app/api/portal/complaints/route.ts | 19 +- src/app/api/portal/expenses/[id]/route.ts | 11 +- src/app/api/portal/expenses/route.ts | 34 +- src/app/api/portal/preferences/route.ts | 17 +- src/app/api/portal/profile/photo/route.ts | 17 +- src/app/api/portal/profile/route.ts | 32 +- src/app/api/portal/proposals/route.ts | 27 +- src/app/api/portal/proposals/vote/route.ts | 33 +- src/app/api/portal/report/route.ts | 53 +- .../api/portal/residents/[id]/photo/route.ts | 44 +- src/app/api/portal/rules/route.ts | 45 +- src/app/api/portal/satisfaction/route.ts | 99 +- src/app/api/portal/settlements/route.ts | 11 +- src/app/api/portal/transfer/route.ts | 40 +- .../[id]/documents/[documentId]/route.ts | 13 +- src/app/portal/chores/[id]/page.tsx | 91 +- src/app/portal/chores/new/page.tsx | 13 +- src/app/portal/chores/page.tsx | 45 +- src/app/portal/decisions/page.tsx | 9 +- src/app/portal/housing/page.tsx | 25 +- src/app/portal/layout.tsx | 9 +- src/app/portal/messages/page.tsx | 9 +- src/app/portal/page.tsx | 104 +- src/app/portal/preferences/page.tsx | 11 +- src/app/portal/profile/page.tsx | 13 +- src/app/portal/report/page.tsx | 21 +- src/app/portal/reports/page.tsx | 19 +- src/app/portal/rules/page.tsx | 9 +- src/app/portal/transfer/page.tsx | 43 +- src/components/governance/AgreementsPanel.tsx | 2 +- src/components/governance/NewProposalForm.tsx | 2 +- src/components/governance/ProposalList.tsx | 2 +- .../governance/ResolutionLadder.tsx | 2 +- .../housing/CompatibilityDetailPopover.tsx | 2 +- src/components/incidents/FollowUpTimeline.tsx | 2 +- src/components/learning/IntegrationBoard.tsx | 2 +- .../matching/ApartmentProfileSection.tsx | 2 +- .../matching/HeadToHeadComparison.tsx | 2 +- src/components/matching/MatchCard.tsx | 2 +- .../matching/ResidentSelectorPanel.tsx | 4 +- src/components/matching/SpotSelection.tsx | 2 +- src/components/matching/UnitModePanel.tsx | 2 +- src/components/portal/MarketplacePostForm.tsx | 2 +- src/components/portal/ProfileForm.tsx | 2 +- .../residents/LearningRecordsCard.tsx | 2 +- .../residents/ResidentIncidents.tsx | 2 +- .../residents/ResidentProfileSidebar.tsx | 8 +- src/lib/actions/care.ts | 260 +- src/lib/actions/complaints.ts | 17 +- src/lib/actions/config.ts | 46 +- src/lib/actions/documents.ts | 42 +- src/lib/actions/events.ts | 102 +- src/lib/actions/governance.ts | 149 +- src/lib/actions/housing.ts | 75 +- src/lib/actions/incidents.ts | 212 +- src/lib/actions/learning.ts | 221 +- src/lib/actions/maintenance.ts | 86 +- src/lib/actions/marketplace.ts | 142 +- src/lib/actions/matching.ts | 80 +- src/lib/actions/opportunities.ts | 154 +- src/lib/actions/placements.ts | 229 +- src/lib/actions/residents.ts | 103 +- src/lib/actions/satisfaction.ts | 44 +- src/lib/actions/spots.ts | 96 +- src/lib/actions/transfers.ts | 45 +- src/lib/ai/staff-chat-tools.ts | 88 +- src/lib/analytics/algorithm-accuracy.ts | 21 +- src/lib/analytics/mission-kpis.ts | 45 +- src/lib/analytics/unit-metrics.ts | 76 +- src/lib/audit.ts | 37 +- src/lib/auth/account.ts | 125 +- src/lib/auth/household.ts | 63 +- src/lib/auth/index.ts | 84 +- src/lib/auth/tokens.ts | 28 +- src/lib/chores/summary.ts | 52 +- src/lib/compatibility/convert.ts | 10 +- src/lib/compatibility/placement-scores.ts | 2 +- src/lib/compatibility/room-fit.ts | 2 +- src/lib/compatibility/save-assessment.ts | 47 +- src/lib/compatibility/types.ts | 4 +- src/lib/config/conflict-resolution.ts | 2 +- src/lib/config/decisions.ts | 2 +- src/lib/config/events.ts | 2 +- src/lib/config/house-rules.ts | 2 +- src/lib/config/household-tasks.ts | 2 +- src/lib/config/marketplace.ts | 2 +- src/lib/constants/labels/complaints.ts | 2 +- src/lib/constants/labels/events.ts | 2 +- src/lib/constants/labels/marketplace.ts | 2 +- src/lib/data/activities.ts | 69 +- src/lib/data/opportunities.ts | 142 +- src/lib/db/helpers.ts | 26 + src/lib/db/index.ts | 1 + src/lib/db/schema.ts | 3802 ++++++++++------- src/lib/demo/reset.ts | 23 +- src/lib/demo/scoped-reset.ts | 60 +- src/lib/demo/seed-data.ts | 1056 ++--- src/lib/demo/seed-governance.ts | 796 ++-- src/lib/demo/staff.ts | 45 +- src/lib/demo/wipe.ts | 14 +- src/lib/env.ts | 2 +- src/lib/expenses/data.ts | 21 +- src/lib/governance/escalation.ts | 2 +- src/lib/governance/lifecycle.ts | 135 +- src/lib/governance/queries.ts | 110 +- src/lib/governance/rules.ts | 2 +- src/lib/governance/sync-org-rules.ts | 39 +- src/lib/governance/voting.ts | 2 +- src/lib/housing/resident-ui.ts | 4 +- src/lib/i18n/portal-surfaces.ts | 2 +- src/lib/matching/types.ts | 2 +- src/lib/messaging/queries.ts | 99 +- src/lib/portal-auth.ts | 60 +- src/lib/privacy/profile-visibility.ts | 2 +- src/lib/reports/resident-reports.ts | 2 +- src/lib/reports/routing.ts | 2 +- src/lib/seed/integration-evidence.ts | 151 +- src/lib/seed/opportunities.ts | 49 +- src/lib/types/index.ts | 11 +- src/lib/validation/schemas.ts | 4 +- src/lib/vulnerability/index.ts | 2 +- 183 files changed, 7403 insertions(+), 5664 deletions(-) create mode 100644 src/lib/db/helpers.ts diff --git a/.env.example b/.env.example index 9547a9c9..62700f09 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,6 @@ DATABASE_URL="postgresql://aoz_wohnen:pass@localhost:5432/aoz_wohnen" # Direct connection URL (for Prisma migrations). On self-hosted Postgres this -# is the same as DATABASE_URL; kept separate so schema.prisma's directUrl works. -DIRECT_URL="postgresql://aoz_wohnen:pass@localhost:5432/aoz_wohnen" # ============================================================================= # AUTHENTICATION @@ -34,7 +32,7 @@ LOGIN_RATE_LIMIT="10" # REQUIRED in production — set as Authorization: Bearer header CRON_SECRET="" -# Initial admin account (used by prisma/seed-admin.ts) +# Initial admin account (used by scripts/db/seed-admin.ts) # Change these before first deploy! ADMIN_CODE="AOZ-ADMIN1" ADMIN_NAME="Admin" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3f5cc25..5f34668e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,6 @@ jobs: - run: npm ci - - name: Generate Prisma client - run: npx prisma generate - - name: Lint run: npm run lint @@ -49,9 +46,6 @@ jobs: - run: npm ci - - name: Generate Prisma client - run: npx prisma generate - - name: Run unit tests run: npm test -- --ci --coverage @@ -82,7 +76,6 @@ jobs: run: npm run build env: DATABASE_URL: 'postgresql://fake:fake@localhost:5432/fake' - DIRECT_URL: 'postgresql://fake:fake@localhost:5432/fake' # Not a real secret: page-data collection imports auth/constants, # whose production fail-fast requires this to be set (>=32 chars). SESSION_SECRET: 'ci-build-only-dummy-0123456789abcdef' @@ -125,16 +118,12 @@ jobs: - run: npm ci - - name: Generate Prisma client - run: npx prisma generate - - name: Initialize database schema - run: npx prisma migrate deploy + run: npm run db:migrate env: DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' - DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' - # Through the npm scripts, never `npx ts-node prisma/…` directly: the + # Through the npm scripts, never `npx ts-node scripts/db/…` directly: the # seed imports the product's compatibility algorithm by its `@/` alias, # and only the scripts carry `-r tsconfig-paths/register`. Invoking # ts-node here by hand was a second definition of "how to seed", and it @@ -142,11 +131,10 @@ jobs: # Guarded by src/lib/__tests__/scoring-ssot.test.ts. - name: Seed E2E data run: | - npm run prisma:seed - npm run prisma:seed:admin + npm run db:seed + npm run db:seed:admin env: DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' - DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' - name: Install Playwright browsers run: npx playwright install --with-deps chromium @@ -155,7 +143,6 @@ jobs: run: npm run test:e2e env: DATABASE_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' - DIRECT_URL: 'postgresql://aoz_test:aoz_test@localhost:5432/aoz_housing_test' SESSION_SECRET: 'ci-test-secret-not-for-production' STAFF_INVITE_CODE: '0000' diff --git a/CLAUDE.md b/CLAUDE.md index d92f5a17..1c968052 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,14 +17,15 @@ The live product is **https://aoz-wohnen.orangecat.ch** on the Hetzner box `/opt/aoz-wohnen/shared/.env` on the box. Neon, Vercel and hosted Supabase were decommissioned on 2026-06-12. A laptop -`.env` that still names `neon.tech` is leftover garbage. Prisma will load it -and time out; that is not "production is down". Do not restore those URLs. +`.env` that still names `neon.tech` is leftover garbage. The db client will +load it and time out; that is not "production is down". Do not restore those URLs. Do not treat gitignored env files as SSOT. This laptop's Postgres is not `aoz_wohnen`. Uncommitted work, and any branch that is not `master`, is not what residents see. Deploy is push to `master` → `.github/workflows/deploy.yml` (waits for -CI, pulls box env, `prisma migrate deploy`, build, rsync). Manual: +CI, applies pending `drizzle/*.sql` via fleetcrown's apply-schema.sh, build, +rsync). Manual: `gh workflow run deploy.yml -R bitbaum/aoz-housing`. Full table: `docs/INFRASTRUCTURE.md`. @@ -136,7 +137,7 @@ This system serves **vulnerable populations** (asylum seekers). Every decision m | Framework | Next.js 16 (App Router) | | Language | TypeScript (strict mode) | | Styling | Tailwind CSS (mobile-first) | -| Database | PostgreSQL + Prisma | +| Database | PostgreSQL + Drizzle ORM | | Validation | Zod (SSOT for types) | | Testing | Jest + Playwright | @@ -449,7 +450,7 @@ guards that specific class. **Building locally needs one env var, and without it the failure lies about its cause.** A bare `npm run build` dies with `Failed to collect configuration -for /api/auth/demo` — a route that queries `prisma.user`, so the obvious +for /api/auth/demo` — a route that queries the user table, so the obvious reading is "this laptop has no `aoz_wohnen` database, builds are impossible here". That reading is wrong, and it was believed twice on 2026-09-01 before anyone read far enough down the log to the real `[cause]`: @@ -554,7 +555,7 @@ export const RESIDENT_FACTORS = { **Maximum 2 files to change:** 1. `src/lib/config/resident-factors.ts` - Define factor -2. `prisma/schema.prisma` - Add column +2. `src/lib/db/schema.ts` - Add column (then `npm run db:generate` for the migration) **If you need to edit more files, the architecture is wrong.** @@ -826,7 +827,7 @@ identity. `src/lib/expenses/` holds the pure logic; routes only do I/O. `simplifyDebts` yields a stable ≤ n−1 transfer plan (greedy, id-tiebreak). - **Resident FKs are `Restrict`, not `Cascade`** — deleting a payer would silently change everyone else's balance. Residents exit via status. -- Categories are **config, not a Prisma enum** (`lib/config/expenses.ts`): +- Categories are **config, not a database enum** (`lib/config/expenses.ts`): a new category is a config change, never a migration. Who may do what: any current unit member records expenses (also on behalf of @@ -842,7 +843,7 @@ portal lets them OPTIONALLY set `displayName`, `bio` and a photo: the SSOT for display — never inline `displayName || code`, and never render `resident.code` directly. Both helpers already fall back to the code, so the privacy default is preserved for free. **Selecting only `{ code: true }` in - Prisma causes the same bug one layer earlier** — spread `RESIDENT_NAME_SELECT` + a query causes the same bug one layer earlier** — spread `RESIDENT_NAME_SELECT` into any query whose rows reach the UI, and use `ResidentSummary` (which carries `displayName`) for compatibility cards. **`NamedResident.displayName` is REQUIRED, not optional — this is the load- @@ -883,7 +884,7 @@ profiles. ### Real deployments vs demo -`prisma/seed-real.ts` seeds a REAL apartment from `prisma/real/*.ts` config +`scripts/db/seed-real.ts` seeds a REAL apartment from `scripts/db/real/*.ts` config (layout + who lives where; login codes are generated at runtime and printed once — never committed). `--wipe` converts a demo instance in place. A real instance must run with `DEMO_ACCESS_ENABLED=false` and the reset timer @@ -1220,7 +1221,8 @@ two reset scopes). ### Database Model -```prisma +``` +// src/lib/db/schema.ts (excerpt, Drizzle; shown here in Prisma-style shorthand) model User { id String @id @default(cuid()) code String @unique // AOZ-XXXXXX login code — the identity @@ -1266,7 +1268,7 @@ model Account { | Audit logging | Active | `src/lib/audit.ts` | | Role switching | Active | UserMenu + PortalNav show cross-links | -**To create initial admin:** Run `npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-admin.ts` (default code: `AOZ-ADMIN1`) +**To create initial admin:** Run `npm run db:seed:admin` (default code: `AOZ-ADMIN1`) --- @@ -1334,11 +1336,11 @@ Representative coverage by area (not an exhaustive suite list): ```bash npm run dev # Development server (port 3001) npm run build # Production build -npm run prisma:generate # Regenerate Prisma client -npm run prisma:migrate # Run pending migrations (production) -npm run prisma:push # Push schema changes (development only) -npm run prisma:studio # Database browser -npm run prisma:seed # Seed demo data +npm run db:generate # Generate a migration from schema.ts changes +npm run db:migrate # Run pending migrations +npm run db:push # Push schema changes (development only) +npm run db:studio # Database browser +npm run db:seed # Seed demo data npm run test # Run Jest tests (2558 tests) npm run test:e2e # Run Playwright tests (173 tests) ``` @@ -1360,7 +1362,7 @@ npm run test:e2e # Run Playwright tests (173 tests) | Transfer actions | `src/lib/actions/transfers.ts` | | Auth guards | `src/lib/auth/index.ts` (`requireStaffAuth()`) | | Route boundaries | `src/lib/auth/route-boundaries.ts` | -| Prisma schema | `prisma/schema.prisma` | +| Drizzle schema | `src/lib/db/schema.ts` | --- @@ -1381,14 +1383,14 @@ npm run test:e2e # Run Playwright tests (173 tests) ## Troubleshooting ### Schema changes workflow -1. Edit `prisma/schema.prisma` -2. Run `npx prisma migrate dev --name describe-change` to create migration -3. Run `npm run prisma:generate` to update client types +1. Edit `src/lib/db/schema.ts` +2. Run `npm run db:generate` to create the migration in `drizzle/` +3. Run `npm run db:migrate` to apply it locally 4. Restart dev server ### "Column not found" errors after schema change -1. Run `npm run prisma:generate` -2. Restart dev server (clears column cache) +1. Run `npm run db:migrate` (the migration may not be applied yet) +2. Restart dev server ### Mobile nav not showing - Check for `sm:hidden` / `hidden sm:flex` patterns diff --git a/DEMO_GUIDE.md b/DEMO_GUIDE.md index c1fc1ebd..3433106d 100644 --- a/DEMO_GUIDE.md +++ b/DEMO_GUIDE.md @@ -196,7 +196,7 @@ This demo showcases the intelligent matching system that reduces conflicts and i ### Before Demo: - [ ] Run `npm run dev` to start server -- [ ] Verify database has demo seed data (`npx ts-node prisma/seed-demo.ts`) +- [ ] Verify database has demo seed data (`npx ts-node scripts/db/seed-demo.ts`) - [ ] Open http://localhost:3000/matching in browser - [ ] Have ROI dashboard (http://localhost:3000/analytics/roi) in second tab - [ ] Test that Ahmed (RES-AH014) appears in resident dropdown @@ -270,7 +270,7 @@ If time permits (extra 2 minutes): **For Questions**: - Technical: Review codebase at /home/g/dev/aoz-housing - Business: Review ROI dashboard at /analytics/roi -- Data: Review seed file at prisma/seed-demo.ts +- Data: Review seed file at scripts/db/seed-demo.ts **Suggested Next Steps**: 1. Pilot with real AOZ data (anonymized) diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md index af3c50aa..ea34145c 100644 --- a/IMPLEMENTATION_SUMMARY.md +++ b/IMPLEMENTATION_SUMMARY.md @@ -79,7 +79,7 @@ This document summarizes all implementation work completed for the AOZ Housing i **File**: `/src/app/matching/page.tsx` (UPDATED) **What it does**: -- Wraps placement operations in `prisma.$transaction()` +- Wraps placement operations in `db.transaction()` - Atomic operations: all-or-nothing - Prevents partial failures leaving database in bad state @@ -130,7 +130,7 @@ This document summarizes all implementation work completed for the AOZ Housing i ## 3. Demo Data ### Comprehensive Seed File ✅ -**File**: `/prisma/seed-demo.ts` (NEW) +**File**: `/scripts/db/seed-demo.ts` (NEW) **What it contains**: - **15 residents**: Including Ahmed (unplaced, demo star) and Maria (transferred 3x) @@ -148,7 +148,7 @@ This document summarizes all implementation work completed for the AOZ Housing i **Real data**: YES - All data exists in database after running seed **Made up**: NO - Data is realistic but fictional for demonstration -**Run with**: `npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-demo.ts` +**Run with**: `npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS","types":["node"]}' scripts/db/seed-demo.ts` --- @@ -300,7 +300,7 @@ This document summarizes all implementation work completed for the AOZ Housing i 2. **Seed demo data** (if not already done): ```bash - npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-demo.ts + npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS","types":["node"]}' scripts/db/seed-demo.ts ``` 3. **Open pages**: @@ -322,7 +322,7 @@ This document summarizes all implementation work completed for the AOZ Housing i 4. `/src/lib/analytics/unit-metrics.ts` 5. `/src/app/analytics/roi/page.tsx` 6. `/src/app/analytics/learning/page.tsx` -7. `/prisma/seed-demo.ts` +7. `/scripts/db/seed-demo.ts` 8. `/DEMO_GUIDE.md` ### Modified Files (5): diff --git a/README.md b/README.md index af86d628..038145ab 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ UI — that stays German. ### Config-Driven Design (2-File Changes) -All factor definitions, labels, options, thresholds, and colors live in `src/lib/config/`. Adding a new compatibility factor requires editing at most 2 files: config + Prisma schema. Forms, scoring, display, and validation auto-generate from config. +All factor definitions, labels, options, thresholds, and colors live in `src/lib/config/`. Adding a new compatibility factor requires editing at most 2 files: config + Drizzle schema. Forms, scoring, display, and validation auto-generate from config. Key config files: - `resident-factors.ts` (550+ lines) -- 38 resident factors organized by form section (basic, lifestyle, social, practical, household, health, preferences, notes) @@ -138,7 +138,7 @@ The system detects when conflicts are likely to emerge and estimates timeframes. |-------|------------| | Framework | Next.js 16 (App Router) | | Language | TypeScript (strict mode) | -| Database | PostgreSQL 17 on Hetzner (`aoz_wohnen`) + Prisma | +| Database | PostgreSQL 17 on Hetzner (`aoz_wohnen`) + Drizzle ORM | | Styling | Tailwind CSS (mobile-first) | | Validation | Zod | | Auth | JWT sessions (bcryptjs + jose) | @@ -162,7 +162,7 @@ The system detects when conflicts are likely to emerge and estimates timeframes. git clone && cd aoz-housing pnpm install cp .env.example .env # local Postgres; production is aoz_wohnen on Hetzner — see docs/INFRASTRUCTURE.md -pnpm prisma migrate deploy +pnpm db:migrate pnpm dev ``` @@ -225,8 +225,8 @@ src/ audit.ts # Placement audit trail auth/ # JWT sessions, role policy, rate limiting components/ # UI components (mobile-first) -prisma/ - schema.prisma # Single source of truth for data model +src/lib/db/ + schema.ts # Single source of truth for data model (drizzle/ holds its SQL migrations) tests/ unit/ # 2341 unit tests (135 suites) e2e/ # 45 Playwright specs (11 files) diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index ff64bab2..2cceea25 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -4,7 +4,7 @@ created_date: 2026-08-17 last_modified_date: 2026-08-19 last_modified_summary: Document fleet AI keys (Groq → OpenRouter); staff chat no longer uses Anthropic. -This file exists because a gitignored laptop `.env` still named a decommissioned Neon host, Prisma loaded it, and an agent treated that timeout as "the production database is unreachable". It was never the production database. +This file exists because a gitignored laptop `.env` still named a decommissioned Neon host, the db client loaded it, and an agent treated that timeout as "the production database is unreachable". It was never the production database. ## Production (the only live instance) @@ -32,8 +32,8 @@ Neon, Vercel and hosted Supabase were left on 2026-06-12 (see `CHANGELOG.md`). T Do not: - Restore those URLs into `.env` -- Treat Prisma's "loaded env from .env" line as proof of where production lives -- Run `prisma migrate` against whatever happens to be in a gitignored file without reading the host +- Treat a client's "loaded env from .env" line as proof of where production lives +- Run migrations against whatever happens to be in a gitignored file without reading the host - Invent a tunnel and then confuse this laptop's Postgres (`aoz_housing`, old scratch DBs) with `aoz_wohnen` on the box ## How code reaches the box @@ -42,7 +42,7 @@ Push to `master` → `.github/workflows/deploy.yml` → reusable `bitbaum/fleetcrown/.github/workflows/selfhost-deploy.yml`. That pipeline waits for this commit's CI, pulls `/opt/aoz-wohnen/shared/.env` -from the box (the box stays env SSOT), runs `prisma migrate deploy` against +from the box (the box stays env SSOT), applies pending `drizzle/*.sql` (fleetcrown apply-schema.sh, ledgered in `_deploy_schema_history`) against `aoz_wohnen` over the deploy tunnel, builds, rsyncs, health-checks. If CI on `master` is red, deploy is blocked. Auto-merge must set @@ -68,10 +68,10 @@ ssh root@167.233.22.31 # then, as the app: cd /opt/aoz-wohnen/current # DATABASE_URL is already aoz_wohnen@localhost -npx prisma migrate status +npm run db:migrate # drizzle-kit; no-ops when the journal is current ``` -Do not point this laptop's Prisma at Neon. Do not assume `localhost:5432` on the laptop is `aoz_wohnen` — that database lives on the box. +Do not point this laptop's db client at Neon. Do not assume `localhost:5432` on the laptop is `aoz_wohnen` — that database lives on the box. Local development uses a **local** Postgres and `.env.example` as the template (`aoz_wohnen` as the name so it matches production). Copy credentials from the box only when you are deliberately tunnelling, and rewrite the host/port to the tunnel — never keep a `neon.tech` host "for convenience". diff --git a/docs/ROLE-WALKTHROUGH-2026-08-31.md b/docs/ROLE-WALKTHROUGH-2026-08-31.md index a6ec9e35..49972bf4 100644 --- a/docs/ROLE-WALKTHROUGH-2026-08-31.md +++ b/docs/ROLE-WALKTHROUGH-2026-08-31.md @@ -35,7 +35,7 @@ rather than leaving the seat quietly empty — an unstaffed domain looks identical to a staffed one nobody has used yet. The SOCIAL seat is covered by Franziska's oversight. -Provisioning lives in `prisma/real/aoz-team.ts` (config) + +Provisioning lives in `scripts/db/real/aoz-team.ts` (config) + `scripts/maintenance/ensure-aoz-team.ts` (idempotent, matches by name, so it is also how a reach is corrected later). Codes are generated at run time and printed once — never committed, for the same reason the real apartment's diff --git a/package.json b/package.json index db018a1e..81c94eeb 100644 --- a/package.json +++ b/package.json @@ -8,26 +8,22 @@ "description": "Support platform for refugee care work: housing stability with compatibility matching, daily-life coordination, conflict resolution, and learning & participation tracking — one system for staff and residents", "scripts": { "dev": "next dev", - "build": "prisma generate && next build", + "build": "next build", "start": "next start", "lint": "eslint .", "typecheck": "tsc --noEmit", "verify": "npm run format:check && npm run lint && npm run typecheck && npm run test", "test": "jest", "test:e2e": "playwright test", - "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate deploy", - "prisma:migrate:dev": "prisma migrate dev", - "prisma:push": "prisma db push", - "prisma:studio": "prisma studio", - "prisma:seed": "prisma db seed", - "prisma:seed:admin": "ts-node -r tsconfig-paths/register --compiler-options '{\"module\":\"CommonJS\",\"types\":[\"node\"]}' prisma/seed-admin.ts", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:seed": "ts-node -r tsconfig-paths/register --compiler-options '{\"module\":\"CommonJS\",\"types\":[\"node\"]}' scripts/db/seed.ts", + "db:seed:admin": "ts-node -r tsconfig-paths/register --compiler-options '{\"module\":\"CommonJS\",\"types\":[\"node\"]}' scripts/db/seed-admin.ts", "format": "prettier --write .", "format:check": "prettier --check ." }, - "prisma": { - "seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\",\"types\":[\"node\"]} prisma/seed.ts" - }, "dependencies": { "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", "@paralleldrive/cuid2": "^3.3.0", diff --git a/scripts/calculate-compatibility.ts b/scripts/calculate-compatibility.ts index ed6757e8..ec99be3a 100644 --- a/scripts/calculate-compatibility.ts +++ b/scripts/calculate-compatibility.ts @@ -3,20 +3,19 @@ * Run with: npx tsx scripts/calculate-compatibility.ts [housing-unit-code] */ -import { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' +import { db, housingUnit, placement, compatibilityAssessment } from '../src/lib/db' import { calculateCompatibility } from '../src/lib/compatibility' import { toResidentProfile } from '../src/lib/compatibility/convert' -const prisma = new PrismaClient() - async function calculateCompatibilityForUnit(unitCode?: string) { // Get housing units (all or specific) - const units = await prisma.housingUnit.findMany({ - where: unitCode ? { code: unitCode } : undefined, - include: { + const units = await db.query.housingUnit.findMany({ + where: unitCode ? eq(housingUnit.code, unitCode) : undefined, + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, }, }) @@ -45,65 +44,41 @@ async function calculateCompatibilityForUnit(unitCode?: string) { console.log(` ${r1.code} <-> ${r2.code}: ${score.overall}%`) + const scores = { + overallScore: score.overall, + lifestyleScore: score.lifestyle, + socialScore: score.social, + practicalScore: score.practical, + riskScore: score.risk, + strengths: score.strengths || [], + concerns: score.concerns || [], + } + // Create/update assessment for r1 -> r2 - await prisma.compatibilityAssessment.upsert({ - where: { - residentId_comparedWithId: { - residentId: r1.id, - comparedWithId: r2.id, - }, - }, - update: { - overallScore: score.overall, - lifestyleScore: score.lifestyle, - socialScore: score.social, - practicalScore: score.practical, - riskScore: score.risk, - strengths: score.strengths || [], - concerns: score.concerns || [], - }, - create: { + await db + .insert(compatibilityAssessment) + .values({ residentId: r1.id, comparedWithId: r2.id, - overallScore: score.overall, - lifestyleScore: score.lifestyle, - socialScore: score.social, - practicalScore: score.practical, - riskScore: score.risk, - strengths: score.strengths || [], - concerns: score.concerns || [], - }, - }) + ...scores, + }) + .onConflictDoUpdate({ + target: [compatibilityAssessment.residentId, compatibilityAssessment.comparedWithId], + set: scores, + }) // Create/update reverse assessment for r2 -> r1 - await prisma.compatibilityAssessment.upsert({ - where: { - residentId_comparedWithId: { - residentId: r2.id, - comparedWithId: r1.id, - }, - }, - update: { - overallScore: score.overall, - lifestyleScore: score.lifestyle, - socialScore: score.social, - practicalScore: score.practical, - riskScore: score.risk, - strengths: score.strengths || [], - concerns: score.concerns || [], - }, - create: { + await db + .insert(compatibilityAssessment) + .values({ residentId: r2.id, comparedWithId: r1.id, - overallScore: score.overall, - lifestyleScore: score.lifestyle, - socialScore: score.social, - practicalScore: score.practical, - riskScore: score.risk, - strengths: score.strengths || [], - concerns: score.concerns || [], - }, - }) + ...scores, + }) + .onConflictDoUpdate({ + target: [compatibilityAssessment.residentId, compatibilityAssessment.comparedWithId], + set: scores, + }) } } } @@ -114,4 +89,4 @@ async function calculateCompatibilityForUnit(unitCode?: string) { const unitCode = process.argv[2] calculateCompatibilityForUnit(unitCode) .catch(console.error) - .finally(() => prisma.$disconnect()) + .finally(() => process.exit(0)) diff --git a/scripts/cleanup-test-data.ts b/scripts/cleanup-test-data.ts index 41a82f18..11141e37 100644 --- a/scripts/cleanup-test-data.ts +++ b/scripts/cleanup-test-data.ts @@ -2,51 +2,48 @@ * Clean up existing test data before running workflow test */ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() +import { eq, like } from 'drizzle-orm' +import { db, resident, housingUnit, placement, incident, placementSpot } from '../src/lib/db' async function main() { console.log('🧹 Cleaning up existing test data...\n') // Delete residents with WIT codes - const deletedResidents = await prisma.resident.deleteMany({ - where: { - code: { - startsWith: 'WIT-', - }, - }, - }) - console.log(`✅ Deleted ${deletedResidents.count} residents with WIT- codes`) + const deletedResidents = await db + .delete(resident) + .where(like(resident.code, 'WIT-%')) + .returning({ id: resident.id }) + console.log(`✅ Deleted ${deletedResidents.length} residents with WIT- codes`) // Find housing unit to delete related data - const unit = await prisma.housingUnit.findUnique({ - where: { code: 'ZH-1-440' }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.code, 'ZH-1-440'), }) if (unit) { // Delete placements first - const deletedPlacements = await prisma.placement.deleteMany({ - where: { housingUnitId: unit.id }, - }) - console.log(`✅ Deleted ${deletedPlacements.count} placements`) + const deletedPlacements = await db + .delete(placement) + .where(eq(placement.housingUnitId, unit.id)) + .returning({ id: placement.id }) + console.log(`✅ Deleted ${deletedPlacements.length} placements`) // Delete incidents - const deletedIncidents = await prisma.incident.deleteMany({ - where: { housingUnitId: unit.id }, - }) - console.log(`✅ Deleted ${deletedIncidents.count} incidents`) + const deletedIncidents = await db + .delete(incident) + .where(eq(incident.housingUnitId, unit.id)) + .returning({ id: incident.id }) + console.log(`✅ Deleted ${deletedIncidents.length} incidents`) // Delete spots - const deletedSpots = await prisma.placementSpot.deleteMany({ - where: { housingUnitId: unit.id }, - }) - console.log(`✅ Deleted ${deletedSpots.count} spots`) + const deletedSpots = await db + .delete(placementSpot) + .where(eq(placementSpot.housingUnitId, unit.id)) + .returning({ id: placementSpot.id }) + console.log(`✅ Deleted ${deletedSpots.length} spots`) // Delete housing unit - await prisma.housingUnit.delete({ - where: { id: unit.id }, - }) + await db.delete(housingUnit).where(eq(housingUnit.id, unit.id)) console.log(`✅ Deleted housing unit ZH-1-440`) } else { console.log('ℹ️ No housing unit ZH-1-440 found') @@ -60,6 +57,6 @@ main() console.error('❌ Error:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/create-witikon-residents.ts b/scripts/create-witikon-residents.ts index 4f2215ea..09ca9ded 100644 --- a/scripts/create-witikon-residents.ts +++ b/scripts/create-witikon-residents.ts @@ -1,27 +1,27 @@ /** * Create 8 diverse residents for Witikon-440 * Following CLAUDE.md best practices: - * - SSOT: Using Prisma schema + * - SSOT: Using the Drizzle schema * - Quality: Type-safe, validated data * - SOC: Data creation separated from business logic */ +import { eq } from 'drizzle-orm' import { - PrismaClient, - AgeRange, - Gender, - FamilyStatus, - SleepSchedule, - SocialStyle, - SmokingStatus, - MobilityNeed, - RecyclingKnowledge, - RoomSharingStatus, - SupportLevel, - ResidentStatus, -} from '@prisma/client' - -const prisma = new PrismaClient() + db, + resident as residentTable, + type AgeRange, + type Gender, + type FamilyStatus, + type SleepSchedule, + type SocialStyle, + type SmokingStatus, + type MobilityNeed, + type RecyclingKnowledge, + type RoomSharingStatus, + type SupportLevel, + type ResidentStatus, +} from '../src/lib/db' async function main() { console.log('🏢 Creating residents for Witikon-440...\n') @@ -170,8 +170,8 @@ async function main() { const { name, ...residentData } = data // Check if already exists - const exists = await prisma.resident.findUnique({ - where: { code: data.code }, + const exists = await db.query.resident.findFirst({ + where: eq(residentTable.code, data.code), }) if (exists) { @@ -179,8 +179,9 @@ async function main() { continue } - const resident = await prisma.resident.create({ - data: { + const [resident] = await db + .insert(residentTable) + .values({ ...residentData, status: 'ACTIVE' as ResidentStatus, // Default values for required fields @@ -196,8 +197,8 @@ async function main() { hasNightDisturbances: false, needsQuietEnvironment: data.noiseTolerance <= 2, hasSleepEquipment: false, - }, - }) + }) + .returning() console.log(` ✓ Created ${resident.code} - ${name}`) } @@ -218,6 +219,6 @@ main() console.error('Error:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/deploy-demo.sh b/scripts/deploy-demo.sh index 45684294..4af69a31 100755 --- a/scripts/deploy-demo.sh +++ b/scripts/deploy-demo.sh @@ -83,14 +83,27 @@ echo "==> migrate the demo database" # box and has to do it itself. Postgres here only accepts 127.0.0.1, so the # migration runs ON the box against its own loopback. # -# The prisma version comes from THIS repo's package.json rather than being -# written here or resolved to `latest` on the box: the migration format has to -# match the client the build was compiled against, and a floating `latest` makes -# the deploy depend on the day it ran. -PRISMA_VERSION=$(node -p "require('./package.json').devDependencies.prisma") -rsync -az --delete prisma/ "$BOX:/opt/aoz-demo/prisma/" -ssh "$BOX" "cd /opt/aoz-demo && set -a && . ./shared/.env && set +a && npx --yes prisma@${PRISMA_VERSION#^} migrate deploy --schema prisma/schema.prisma" \ +# Drizzle migrations are plain forward-only SQL, applied with psql against the +# demo's own DATABASE_URL and ledgered in public._deploy_schema_history — the +# same ledger fleetcrown's apply-schema.sh keeps for the real instance, so both +# databases answer "what has been applied" the same way. Each file runs in a +# single transaction (-1); a failure aborts the deploy before the restart. +rsync -az --delete drizzle/ "$BOX:/opt/aoz-demo/drizzle/" +ssh "$BOX" 'bash -s' <<'EOSH' \ || { echo "migration failed — NOT restarting into a schema the code cannot use"; exit 1; } +set -euo pipefail +cd /opt/aoz-demo && set -a && . ./shared/.env && set +a +psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -qc \ + "CREATE TABLE IF NOT EXISTS public._deploy_schema_history (tag text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())" +for f in drizzle/[0-9]*.sql; do + tag=$(basename "$f" .sql) + applied=$(psql "$DATABASE_URL" -tAc "SELECT 1 FROM public._deploy_schema_history WHERE tag = '$tag'") + [ "$applied" = "1" ] && { echo " $tag: already applied"; continue; } + echo " $tag: applying" + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -1 -f "$f" + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -qc "INSERT INTO public._deploy_schema_history(tag) VALUES ('$tag')" +done +EOSH echo "==> restart" ssh "$BOX" "chown -R ubuntu:ubuntu $APP_DIR && systemctl restart aoz-demo-app" diff --git a/scripts/maintenance/ensure-aoz-team.ts b/scripts/maintenance/ensure-aoz-team.ts index 6a14558f..ec9d64f7 100644 --- a/scripts/maintenance/ensure-aoz-team.ts +++ b/scripts/maintenance/ensure-aoz-team.ts @@ -13,13 +13,13 @@ * Add DRY_RUN=1 to see what it would do without writing. */ -import { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' +import { db, user } from '../../src/lib/db' import { AOZ_TEAM } from '../../prisma/real/aoz-team' import { BRAND } from '../../src/lib/config/brand' import { generateStaffCode } from '../../src/lib/auth/code-generation' import { CARE_ROLES, CARE_ROLE_LABELS, STAFF_ROLE_CARE_DOMAIN } from '../../src/lib/config/care' -const prisma = new PrismaClient() const DRY_RUN = process.env.DRY_RUN === '1' /** @@ -53,7 +53,7 @@ function requireExplicitBrand(): void { async function uniqueStaffCode(): Promise { for (let attempt = 0; attempt < 10; attempt++) { const code = generateStaffCode() - if (!(await prisma.user.findUnique({ where: { code }, select: { id: true } }))) { + if (!(await db.query.user.findFirst({ where: eq(user.code, code), columns: { id: true } }))) { return code } } @@ -73,9 +73,9 @@ async function main() { isSystemAdmin: person.isSystemAdmin, } - const existing = await prisma.user.findFirst({ - where: { name: person.name }, - select: { id: true, code: true, role: true, scope: true, isSystemAdmin: true, active: true }, + const existing = await db.query.user.findFirst({ + where: eq(user.name, person.name), + columns: { id: true, code: true, role: true, scope: true, isSystemAdmin: true, active: true }, }) if (existing) { @@ -95,10 +95,10 @@ async function main() { ` -> ${person.role}/${person.scope}${person.isSystemAdmin ? '/admin' : ''}`, ) if (!DRY_RUN) { - await prisma.user.update({ - where: { id: existing.id }, - data: { ...capabilities, active: true }, - }) + await db + .update(user) + .set({ ...capabilities, active: true }) + .where(eq(user.id, existing.id)) } continue } @@ -110,9 +110,7 @@ async function main() { const code = DRY_RUN ? `${BRAND.codePrefix}` : await uniqueStaffCode() console.log(`+ ${person.name}: ${person.role}/${person.scope}`) if (!DRY_RUN) { - await prisma.user.create({ - data: { code, name: person.name, ...capabilities, active: true }, - }) + await db.insert(user).values({ code, name: person.name, ...capabilities, active: true }) minted.push({ name: person.name, code }) } } @@ -156,6 +154,6 @@ main() console.error('ensure-aoz-team failed:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/maintenance/ensure-operator.ts b/scripts/maintenance/ensure-operator.ts index 64a02516..c1cc7479 100644 --- a/scripts/maintenance/ensure-operator.ts +++ b/scripts/maintenance/ensure-operator.ts @@ -10,11 +10,11 @@ * OPERATOR_RESIDENT_NAME=Georgy npx ts-node --compiler-options '{"module":"CommonJS"}' scripts/maintenance/ensure-operator.ts */ -import { PrismaClient } from '@prisma/client' +import { and, asc, eq, inArray } from 'drizzle-orm' +import { db, user, account, careAssignment, resident as residentTable } from '../../src/lib/db' import { generateStaffCode } from '../../src/lib/auth/code-generation' import { CARE_ROLES } from '../../src/lib/config/care' -const prisma = new PrismaClient() const RESIDENT_NAME = process.env.OPERATOR_RESIDENT_NAME || 'Georgy' /** @@ -36,7 +36,7 @@ const OPERATOR_CAPABILITIES = { async function uniqueStaffCode(): Promise { for (let attempt = 0; attempt < 10; attempt++) { const code = generateStaffCode() - if (!(await prisma.user.findUnique({ where: { code }, select: { id: true } }))) { + if (!(await db.query.user.findFirst({ where: eq(user.code, code), columns: { id: true } }))) { return code } } @@ -44,77 +44,82 @@ async function uniqueStaffCode(): Promise { } async function main() { - const resident = await prisma.resident.findFirst({ - where: { displayName: RESIDENT_NAME, status: { in: ['ACTIVE', 'PLACED'] } }, - include: { account: true }, - orderBy: { createdAt: 'asc' }, + const resident = await db.query.resident.findFirst({ + where: and( + eq(residentTable.displayName, RESIDENT_NAME), + inArray(residentTable.status, ['ACTIVE', 'PLACED']), + ), + orderBy: [asc(residentTable.createdAt)], }) if (!resident) { throw new Error(`No active resident named ${RESIDENT_NAME}`) } - let staffId = resident.account?.userId ?? null + // Fetched directly (Account.residentId is unique) rather than via `with`, + // whose inferred type for a one() relation declared without fields is too + // loose to read `.userId` off. + const residentAccount = + (await db.query.account.findFirst({ where: eq(account.residentId, resident.id) })) ?? null + + let staffId = residentAccount?.userId ?? null let staffCode: string | null = null let createdStaff = false if (staffId) { - const existing = await prisma.user.findUnique({ - where: { id: staffId }, - select: { id: true, code: true, role: true, active: true }, + const existing = await db.query.user.findFirst({ + where: eq(user.id, staffId), + columns: { id: true, code: true, role: true, active: true }, }) if (!existing?.active) { throw new Error('Linked staff identity is missing or inactive') } staffCode = existing.code - await prisma.user.update({ where: { id: existing.id }, data: OPERATOR_CAPABILITIES }) + await db.update(user).set(OPERATOR_CAPABILITIES).where(eq(user.id, existing.id)) } else { - const named = await prisma.user.findFirst({ - where: { name: resident.displayName || RESIDENT_NAME, active: true }, - select: { id: true, code: true, account: { select: { id: true } } }, + const named = await db.query.user.findFirst({ + where: and(eq(user.name, resident.displayName || RESIDENT_NAME), eq(user.active, true)), + columns: { id: true, code: true }, + with: { account: { columns: { id: true } } }, }) if (named && !named.account) { staffId = named.id staffCode = named.code - await prisma.user.update({ where: { id: named.id }, data: OPERATOR_CAPABILITIES }) + await db.update(user).set(OPERATOR_CAPABILITIES).where(eq(user.id, named.id)) } else { const code = await uniqueStaffCode() - const created = await prisma.user.create({ - data: { + const [created] = await db + .insert(user) + .values({ code, name: resident.displayName || RESIDENT_NAME, ...OPERATOR_CAPABILITIES, active: true, - }, - }) + }) + .returning() staffId = created.id staffCode = created.code createdStaff = true } - if (resident.account) { - await prisma.account.update({ - where: { id: resident.account.id }, - data: { userId: staffId }, - }) + if (residentAccount) { + await db.update(account).set({ userId: staffId }).where(eq(account.id, residentAccount.id)) } } - const residents = await prisma.resident.findMany({ - where: { status: { in: ['ACTIVE', 'PLACED'] } }, - select: { id: true, displayName: true }, + const residents = await db.query.resident.findMany({ + where: inArray(residentTable.status, ['ACTIVE', 'PLACED']), + columns: { id: true, displayName: true }, }) let seatsFilled = 0 for (const person of residents) { for (const role of CARE_ROLES) { - const existing = await prisma.careAssignment.findUnique({ - where: { residentId_role: { residentId: person.id, role } }, - select: { id: true }, + const existing = await db.query.careAssignment.findFirst({ + where: and(eq(careAssignment.residentId, person.id), eq(careAssignment.role, role)), + columns: { id: true }, }) if (existing) continue - await prisma.careAssignment.create({ - data: { residentId: person.id, staffId: staffId!, role }, - }) + await db.insert(careAssignment).values({ residentId: person.id, staffId: staffId!, role }) seatsFilled += 1 } } @@ -123,7 +128,7 @@ async function main() { console.log(`Staff ${staffCode} — Leitung, all three care domains`) if (createdStaff) { console.log('New staff code created. Register it with the SAME email as the resident account.') - } else if (resident.account?.userId || resident.account) { + } else if (residentAccount?.userId || residentAccount) { console.log( 'Staff identity is on the same login as the resident. Sign in once, switch in the nav.', ) @@ -140,6 +145,6 @@ main() console.error(error) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/seed-witikon.ts b/scripts/seed-witikon.ts index e2bd28c0..e4db33c3 100644 --- a/scripts/seed-witikon.ts +++ b/scripts/seed-witikon.ts @@ -1,12 +1,17 @@ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() +import { and, eq } from 'drizzle-orm' +import { + db, + housingUnit, + resident as residentTable, + placement as placementTable, + placementSpot, +} from '../src/lib/db' async function main() { // Find the housing unit WIT-440 - const housing = await prisma.housingUnit.findUnique({ - where: { code: 'WIT-440' }, - include: { spots: true }, + const housing = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.code, 'WIT-440'), + with: { spots: true }, }) if (!housing) { @@ -96,19 +101,20 @@ async function main() { for (const code of residents) { // Check if resident already exists - const existing = await prisma.resident.findUnique({ where: { code } }) + const existing = await db.query.resident.findFirst({ where: eq(residentTable.code, code) }) if (existing) { console.log(`Resident ${code} already exists, skipping creation`) createdResidents[code] = existing continue } - const resident = await prisma.resident.create({ - data: { + const [resident] = await db + .insert(residentTable) + .values({ code, ...defaultData, - }, - }) + }) + .returning() console.log(`Created resident: ${code} (${resident.id})`) createdResidents[code] = resident } @@ -135,11 +141,8 @@ async function main() { const resident = createdResidents[p.resident] // Check if placement already exists - const existingPlacement = await prisma.placement.findFirst({ - where: { - residentId: resident.id, - status: 'ACTIVE', - }, + const existingPlacement = await db.query.placement.findFirst({ + where: and(eq(placementTable.residentId, resident.id), eq(placementTable.status, 'ACTIVE')), }) if (existingPlacement) { @@ -147,27 +150,22 @@ async function main() { continue } - const placement = await prisma.placement.create({ - data: { - residentId: resident.id, - housingUnitId: housing.id, - spotId: p.bed.id, - startDate: new Date(), - status: 'ACTIVE', - }, + await db.insert(placementTable).values({ + residentId: resident.id, + housingUnitId: housing.id, + spotId: p.bed.id, + startDate: new Date(), + status: 'ACTIVE', }) // Update spot status to OCCUPIED - await prisma.placementSpot.update({ - where: { id: p.bed.id }, - data: { status: 'OCCUPIED' }, - }) + await db.update(placementSpot).set({ status: 'OCCUPIED' }).where(eq(placementSpot.id, p.bed.id)) // Update resident status to PLACED - await prisma.resident.update({ - where: { id: resident.id }, - data: { status: 'PLACED' }, - }) + await db + .update(residentTable) + .set({ status: 'PLACED' }) + .where(eq(residentTable.id, resident.id)) console.log(`Created placement: ${p.resident} -> ${p.bed.code}`) } @@ -177,4 +175,4 @@ async function main() { main() .catch(console.error) - .finally(() => prisma.$disconnect()) + .finally(() => process.exit(0)) diff --git a/scripts/test-troublemaker-detection.ts b/scripts/test-troublemaker-detection.ts index 0ae7c913..b5534e8c 100644 --- a/scripts/test-troublemaker-detection.ts +++ b/scripts/test-troublemaker-detection.ts @@ -2,16 +2,15 @@ * Test troublemaker detection by creating multiple incidents about Carlos */ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() +import { eq, or } from 'drizzle-orm' +import { db, resident, housingUnit, incident as incidentTable } from '../src/lib/db' async function main() { console.log('🔍 Testing troublemaker detection workflow...\n') // Find Carlos (WIT-005) - the party person - const carlos = await prisma.resident.findUnique({ - where: { code: 'WIT-005' }, + const carlos = await db.query.resident.findFirst({ + where: eq(resident.code, 'WIT-005'), }) if (!carlos) { @@ -20,8 +19,8 @@ async function main() { } // Find the housing unit - const unit = await prisma.housingUnit.findUnique({ - where: { code: 'ZH-1-440' }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.code, 'ZH-1-440'), }) if (!unit) { @@ -30,54 +29,48 @@ async function main() { } // Find other residents to report incidents - const ahmed = await prisma.resident.findUnique({ where: { code: 'WIT-001' } }) - const john = await prisma.resident.findUnique({ where: { code: 'WIT-007' } }) - const yuki = await prisma.resident.findUnique({ where: { code: 'WIT-008' } }) + const ahmed = await db.query.resident.findFirst({ where: eq(resident.code, 'WIT-001') }) + const john = await db.query.resident.findFirst({ where: eq(resident.code, 'WIT-007') }) + const yuki = await db.query.resident.findFirst({ where: eq(resident.code, 'WIT-008') }) console.log('📊 Creating multiple incidents about Carlos to trigger warning...\n') // Incident 2: Cleanliness issue - const incident2 = await prisma.incident.create({ - data: { - housingUnitId: unit.id, - reportedById: john!.id, - subjectId: carlos.id, - category: 'INTERPERSONAL', - type: 'CLEANLINESS_DISPUTE', - severity: 'MEDIUM', - description: 'Carlos left dirty dishes in the kitchen for 3 days', - date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago - }, + await db.insert(incidentTable).values({ + housingUnitId: unit.id, + reportedById: john!.id, + subjectId: carlos.id, + category: 'INTERPERSONAL', + type: 'CLEANLINESS_DISPUTE', + severity: 'MEDIUM', + description: 'Carlos left dirty dishes in the kitchen for 3 days', + date: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), // 2 days ago }) console.log('✅ Incident 2: CLEANLINESS_DISPUTE') console.log(` Reported by: ${john!.code}`) console.log(` Subject: ${carlos.code}`) // Incident 3: Another noise complaint - const incident3 = await prisma.incident.create({ - data: { - housingUnitId: unit.id, - reportedById: yuki!.id, - subjectId: carlos.id, - category: 'INTERPERSONAL', - type: 'NOISE_COMPLAINT', - severity: 'HIGH', - description: 'Carlos had a party with friends, very loud until 4 AM', - date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago - }, + await db.insert(incidentTable).values({ + housingUnitId: unit.id, + reportedById: yuki!.id, + subjectId: carlos.id, + category: 'INTERPERSONAL', + type: 'NOISE_COMPLAINT', + severity: 'HIGH', + description: 'Carlos had a party with friends, very loud until 4 AM', + date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // 1 day ago }) console.log('✅ Incident 3: NOISE_COMPLAINT') console.log(` Reported by: ${yuki!.code}`) console.log(` Subject: ${carlos.code}`) // Get stats for Carlos - const stats = await prisma.incident.findMany({ - where: { - OR: [{ reportedById: carlos.id }, { subjectId: carlos.id }], - }, - include: { - reportedBy: { select: { code: true } }, - subject: { select: { code: true } }, + const stats = await db.query.incident.findMany({ + where: or(eq(incidentTable.reportedById, carlos.id), eq(incidentTable.subjectId, carlos.id)), + with: { + reportedBy: { columns: { code: true } }, + subject: { columns: { code: true } }, }, }) @@ -89,10 +82,10 @@ async function main() { console.log(`\n⚠️ Carlos should now show a WARNING in the UI (3+ incidents as subject)`) // Check housing unit frequent subjects - const allIncidents = await prisma.incident.findMany({ - where: { housingUnitId: unit.id }, - include: { - subject: { select: { code: true } }, + const allIncidents = await db.query.incident.findMany({ + where: eq(incidentTable.housingUnitId, unit.id), + with: { + subject: { columns: { code: true } }, }, }) @@ -124,6 +117,6 @@ main() console.error('❌ Error:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/test-workflow.ts b/scripts/test-workflow.ts index b09ab8d5..81c72c41 100644 --- a/scripts/test-workflow.ts +++ b/scripts/test-workflow.ts @@ -3,30 +3,36 @@ * Tests: Housing creation, spots, residents, placements, incidents */ +import { and, asc, eq, inArray } from 'drizzle-orm' import { - PrismaClient, - AgeRange, - Gender, - FamilyStatus, - SleepSchedule, - SocialStyle, - SmokingStatus, - MobilityNeed, - HousingStatus, - SpotType, - SpotStatus, - MedicalDocType, -} from '@prisma/client' - -const prisma = new PrismaClient() + db, + housingUnit as housingUnitTable, + placementSpot, + resident as residentTable, + placement as placementTable, + incident as incidentTable, + type Incident, + type AgeRange, + type Gender, + type FamilyStatus, + type SleepSchedule, + type SocialStyle, + type SmokingStatus, + type MobilityNeed, + type HousingStatus, + type SpotType, + type SpotStatus, + type MedicalDocType, +} from '../src/lib/db' async function main() { console.log('🧪 Starting comprehensive workflow test...\n') // TASK 1: Create Housing Unit console.log('📍 TASK 1: Creating housing unit ZH-1-440...') - const housingUnit = await prisma.housingUnit.create({ - data: { + const [housingUnit] = await db + .insert(housingUnitTable) + .values({ code: 'ZH-1-440', address: 'Witikonerstrasse 440, 8053 Zürich', totalBeds: 8, @@ -47,8 +53,8 @@ async function main() { nearHealthServices: true, nearSchools: false, status: 'AVAILABLE' as HousingStatus, - }, - }) + }) + .returning() console.log(`✅ Created housing unit: ${housingUnit.code}`) console.log(` ID: ${housingUnit.id}\n`) @@ -56,22 +62,24 @@ async function main() { console.log('🛏️ TASK 2: Creating placement spots...') // Room 1: 3-bed shared room - const room1 = await prisma.placementSpot.create({ - data: { + const [room1] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, code: 'R1', label: 'Zimmer 1 (3-Bett)', type: 'ROOM' as SpotType, capacity: 3, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(`✅ Created ${room1.label}`) // Create 3 beds in Room 1 for (let i = 1; i <= 3; i++) { - const bed = await prisma.placementSpot.create({ - data: { + const [bed] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, parentSpotId: room1.id, code: `R1-B${i}`, @@ -79,27 +87,29 @@ async function main() { type: 'BED' as SpotType, capacity: 1, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(` ✅ Created ${bed.code}`) } // Room 2: 2-bed shared room - const room2 = await prisma.placementSpot.create({ - data: { + const [room2] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, code: 'R2', label: 'Zimmer 2 (2-Bett)', type: 'ROOM' as SpotType, capacity: 2, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(`✅ Created ${room2.label}`) for (let i = 1; i <= 2; i++) { - const bed = await prisma.placementSpot.create({ - data: { + const [bed] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, parentSpotId: room2.id, code: `R2-B${i}`, @@ -107,27 +117,29 @@ async function main() { type: 'BED' as SpotType, capacity: 1, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(` ✅ Created ${bed.code}`) } // Room 3: 2-bed shared room - const room3 = await prisma.placementSpot.create({ - data: { + const [room3] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, code: 'R3', label: 'Zimmer 3 (2-Bett)', type: 'ROOM' as SpotType, capacity: 2, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(`✅ Created ${room3.label}`) for (let i = 1; i <= 2; i++) { - const bed = await prisma.placementSpot.create({ - data: { + const [bed] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, parentSpotId: room3.id, code: `R3-B${i}`, @@ -135,14 +147,15 @@ async function main() { type: 'BED' as SpotType, capacity: 1, status: 'AVAILABLE' as SpotStatus, - }, - }) + }) + .returning() console.log(` ✅ Created ${bed.code}`) } // Room 4: 1-bed private room (medical documentation required) - const room4 = await prisma.placementSpot.create({ - data: { + const [room4] = await db + .insert(placementSpot) + .values({ housingUnitId: housingUnit.id, code: 'R4', label: 'Privatzimmer (med. Attest)', @@ -151,8 +164,8 @@ async function main() { status: 'AVAILABLE' as SpotStatus, requiresMedicalDocs: true, hasPrivateToilet: true, - }, - }) + }) + .returning() console.log(`✅ Created ${room4.label} (requires medical docs)\n`) // TASK 3: Create 8 Diverse Residents @@ -319,8 +332,9 @@ async function main() { const createdResidents = [] for (const residentData of residents) { const { description, ...data } = residentData - const resident = await prisma.resident.create({ - data: { + const [resident] = await db + .insert(residentTable) + .values({ ...data, status: 'ACTIVE', choresContribution: 3, @@ -335,8 +349,8 @@ async function main() { needsQuietEnvironment: residentData.noiseTolerance <= 2, hasSleepEquipment: false, notes: description, - }, - }) + }) + .returning() createdResidents.push(resident) console.log(`✅ Created ${resident.code}: ${description}`) } @@ -346,12 +360,12 @@ async function main() { console.log('🏠 TASK 4: Placing residents into beds...') // Get all available beds - const beds = await prisma.placementSpot.findMany({ - where: { - housingUnitId: housingUnit.id, - type: { in: ['BED', 'PRIVATE_ROOM'] }, - }, - orderBy: { code: 'asc' }, + const beds = await db.query.placementSpot.findMany({ + where: and( + eq(placementSpot.housingUnitId, housingUnit.id), + inArray(placementSpot.type, ['BED', 'PRIVATE_ROOM']), + ), + orderBy: [asc(placementSpot.code)], }) console.log(`Found ${beds.length} available beds\n`) @@ -360,24 +374,22 @@ async function main() { const privateRoomResident = createdResidents.find((r) => r.hasMedicalDocumentation)! const privateRoom = beds.find((b) => b.requiresMedicalDocs)! - const placement1 = await prisma.placement.create({ - data: { - residentId: privateRoomResident.id, - housingUnitId: housingUnit.id, - spotId: privateRoom.id, - startDate: new Date(), - status: 'ACTIVE', - placementNotes: 'Private room due to medical documentation', - }, - }) - await prisma.placementSpot.update({ - where: { id: privateRoom.id }, - data: { status: 'OCCUPIED' }, - }) - await prisma.resident.update({ - where: { id: privateRoomResident.id }, - data: { status: 'PLACED' }, + await db.insert(placementTable).values({ + residentId: privateRoomResident.id, + housingUnitId: housingUnit.id, + spotId: privateRoom.id, + startDate: new Date(), + status: 'ACTIVE', + placementNotes: 'Private room due to medical documentation', }) + await db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, privateRoom.id)) + await db + .update(residentTable) + .set({ status: 'PLACED' }) + .where(eq(residentTable.id, privateRoomResident.id)) console.log(`✅ Placed ${privateRoomResident.code} in ${privateRoom.code} (private room)`) // Place remaining 7 residents in regular beds @@ -388,23 +400,18 @@ async function main() { const resident = regularResidents[i] const bed = regularBeds[i] - const placement = await prisma.placement.create({ - data: { - residentId: resident.id, - housingUnitId: housingUnit.id, - spotId: bed.id, - startDate: new Date(), - status: 'ACTIVE', - }, - }) - await prisma.placementSpot.update({ - where: { id: bed.id }, - data: { status: 'OCCUPIED' }, - }) - await prisma.resident.update({ - where: { id: resident.id }, - data: { status: 'PLACED' }, + await db.insert(placementTable).values({ + residentId: resident.id, + housingUnitId: housingUnit.id, + spotId: bed.id, + startDate: new Date(), + status: 'ACTIVE', }) + await db.update(placementSpot).set({ status: 'OCCUPIED' }).where(eq(placementSpot.id, bed.id)) + await db + .update(residentTable) + .set({ status: 'PLACED' }) + .where(eq(residentTable.id, resident.id)) console.log(`✅ Placed ${resident.code} in ${bed.code}`) } console.log() @@ -412,20 +419,20 @@ async function main() { // TASK 5: Verify Final State console.log('✅ TASK 5: Verifying final state...') - const finalUnit = await prisma.housingUnit.findUnique({ - where: { id: housingUnit.id }, - include: { + const finalUnit = await db.query.housingUnit.findFirst({ + where: eq(housingUnitTable.id, housingUnit.id), + with: { spots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placementTable.status, 'ACTIVE'), + with: { resident: true }, }, }, }, placements: { - where: { status: 'ACTIVE' }, - include: { resident: true, spot: true }, + where: eq(placementTable.status, 'ACTIVE'), + with: { resident: true, spot: true }, }, }, }) @@ -442,8 +449,11 @@ async function main() { // Test incident creation console.log(`\n🚨 Testing incident reporting...`) - const incident = await prisma.incident.create({ - data: { + // Cast: the incident table's inferred type degrades to `any` because of the + // placement<->incident FK cycle in schema.ts, which breaks .returning() inference. + const [incident] = (await db + .insert(incidentTable) + .values({ housingUnitId: housingUnit.id, reportedById: createdResidents[0].id, subjectId: createdResidents[4].id, // Carlos - the party person @@ -452,8 +462,8 @@ async function main() { severity: 'MEDIUM', description: 'Carlos played loud music at 2 AM', date: new Date(), - }, - }) + }) + .returning()) as Incident[] console.log(`✅ Created incident: ${incident.type}`) console.log(` Reported by: ${createdResidents[0].code}`) console.log(` About: ${createdResidents[4].code}`) @@ -474,6 +484,6 @@ main() console.error('❌ Error:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/scripts/verify-data.ts b/scripts/verify-data.ts index f279bbaf..f74640ba 100644 --- a/scripts/verify-data.ts +++ b/scripts/verify-data.ts @@ -2,26 +2,25 @@ * Verify all test data is correctly set up */ -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() +import { eq, like } from 'drizzle-orm' +import { db, housingUnit, resident, placement } from '../src/lib/db' async function main() { console.log('🔍 Verifying test data...\n') // Check housing unit - const unit = await prisma.housingUnit.findUnique({ - where: { code: 'ZH-1-440' }, - include: { + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.code, 'ZH-1-440'), + with: { spots: true, placements: { - where: { status: 'ACTIVE' }, - include: { resident: true, spot: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true, spot: true }, }, incidents: { - include: { - reportedBy: { select: { code: true } }, - subject: { select: { code: true } }, + with: { + reportedBy: { columns: { code: true } }, + subject: { columns: { code: true } }, }, }, }, @@ -69,13 +68,11 @@ async function main() { }) // Check residents - const residents = await prisma.resident.findMany({ - where: { - code: { startsWith: 'WIT-' }, - }, - include: { + const residents = await db.query.resident.findMany({ + where: like(resident.code, 'WIT-%'), + with: { placements: { - where: { status: 'ACTIVE' }, + where: eq(placement.status, 'ACTIVE'), }, incidentsAsSubject: true, incidentsReported: true, @@ -109,6 +106,6 @@ main() console.error('❌ Error:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() + .finally(() => { + process.exit(0) }) diff --git a/src/app/(admin)/analytics/page.tsx b/src/app/(admin)/analytics/page.tsx index 46eb78e9..278ae353 100644 --- a/src/app/(admin)/analytics/page.tsx +++ b/src/app/(admin)/analytics/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, resident, housingUnit, placement, incident, satisfactionCheckIn } from '@/lib/db' +import { eq, ne, and, inArray, gte, desc } from 'drizzle-orm' import Link from 'next/link' export const metadata: Metadata = { title: 'Statistiken' } @@ -56,60 +57,60 @@ export default async function AnalyticsPage({ searchParams }: Props) { const [residents, units, placements, recentPlacements, recentIncidents, checkIns] = await Promise.all([ - prisma.resident.findMany({ - where: { status: { in: ['ACTIVE', 'PLACED'] } }, + db.query.resident.findMany({ + where: inArray(resident.status, ['ACTIVE', 'PLACED']), }), - prisma.housingUnit.findMany({ - include: { placements: { where: { status: 'ACTIVE' } } }, + db.query.housingUnit.findMany({ + with: { placements: { where: eq(placement.status, 'ACTIVE') } }, }), - prisma.placement.findMany({ - where: { status: 'ACTIVE' }, - include: { + db.query.placement.findMany({ + where: eq(placement.status, 'ACTIVE'), + with: { resident: true, housingUnit: true, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, }, }, }), canReadPlacements - ? prisma.placement.findMany({ - where: { startDate: { gte: ninetyDaysAgo } }, - include: { + ? db.query.placement.findMany({ + where: gte(placement.startDate, ninetyDaysAgo), + with: { housingUnit: true, resident: true, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, }, }, - orderBy: { startDate: 'desc' }, + orderBy: [desc(placement.startDate)], }) : [], - prisma.incident.findMany({ - where: { - date: { gte: periodStart }, - category: 'INTERPERSONAL', // Only conflicts, not maintenance - }, - include: { housingUnit: true }, + db.query.incident.findMany({ + where: and( + gte(incident.date, periodStart), + eq(incident.category, 'INTERPERSONAL'), // Only conflicts, not maintenance + ), + with: { housingUnit: true }, }), - prisma.satisfactionCheckIn.findMany({ - where: { createdAt: { gte: periodStart } }, - include: { + db.query.satisfactionCheckIn.findMany({ + where: gte(satisfactionCheckIn.createdAt, periodStart), + with: { placement: { - include: { resident: true, housingUnit: true }, + with: { resident: true, housingUnit: true }, }, }, - orderBy: { createdAt: 'desc' }, + orderBy: [desc(satisfactionCheckIn.createdAt)], }), ]) // Get all ended placements for end reason analysis (including conflict analysis fields) const [endedPlacements, missionKPIs, algorithmAccuracy, systemConfig] = await Promise.all([ - prisma.placement.findMany({ - where: { status: { not: 'ACTIVE' } }, - select: { + db.query.placement.findMany({ + where: ne(placement.status, 'ACTIVE'), + columns: { endReason: true, conflictGap: true, wasPredictable: true, diff --git a/src/app/(admin)/chores/new/page.tsx b/src/app/(admin)/chores/new/page.tsx index ecfa671c..97967d81 100644 --- a/src/app/(admin)/chores/new/page.tsx +++ b/src/app/(admin)/chores/new/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit } from '@/lib/db' +import { inArray, asc } from 'drizzle-orm' import { PageHeader, EmptyState } from '@/components/ui/Page' import { StaffChoreForm } from '@/components/housing/StaffChoreForm' import { CHORE_LABELS } from '@/lib/config/household-tasks' @@ -19,10 +20,10 @@ export const dynamic = 'force-dynamic' * agreement stayed in their notes and the house never saw it. */ export default async function NewStaffChorePage() { - const units = await prisma.housingUnit.findMany({ - where: { status: { in: ['AVAILABLE', 'FULL'] } }, - select: { id: true, ...UNIT_NAME_SELECT }, - orderBy: { code: 'asc' }, + const units = await db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + columns: { id: true, ...UNIT_NAME_SELECT }, + orderBy: [asc(housingUnit.code)], }) return ( diff --git a/src/app/(admin)/chores/page.tsx b/src/app/(admin)/chores/page.tsx index 5692a942..dedc2fc2 100644 --- a/src/app/(admin)/chores/page.tsx +++ b/src/app/(admin)/chores/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, householdTask, taskCompletion, housingUnit, placement } from '@/lib/db' +import { eq, inArray, asc } from 'drizzle-orm' import Link from 'next/link' import { StatCard } from '@/components/ui/Card' import { PageHeader } from '@/components/ui/Page' @@ -16,34 +17,34 @@ export default async function AdminChoresPage() { await requirePermission('housing:read') // Overall stats const [totalTasks, activeTasks, attentionTasks, totalCompletions] = await Promise.all([ - prisma.householdTask.count(), - prisma.householdTask.count({ where: { isCompleted: false } }), - prisma.householdTask.count({ where: { currentStatus: 'NEEDS_ATTENTION' } }), - prisma.taskCompletion.count(), + db.$count(householdTask), + db.$count(householdTask, eq(householdTask.isCompleted, false)), + db.$count(householdTask, eq(householdTask.currentStatus, 'NEEDS_ATTENTION')), + db.$count(taskCompletion), ]) // Per-unit summary - const units = await prisma.housingUnit.findMany({ - where: { - status: { in: ['AVAILABLE', 'FULL'] }, - }, - select: { + const units = await db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + columns: { id: true, code: true, address: true, + }, + with: { householdTasks: { - select: { + columns: { id: true, currentStatus: true, isCompleted: true, }, }, placements: { - where: { status: 'ACTIVE' }, - select: { id: true }, + where: eq(placement.status, 'ACTIVE'), + columns: { id: true }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }) const unitSummaries = units diff --git a/src/app/(admin)/complaints/page.tsx b/src/app/(admin)/complaints/page.tsx index db4fe5e3..7fcd0f39 100644 --- a/src/app/(admin)/complaints/page.tsx +++ b/src/app/(admin)/complaints/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, complaint } from '@/lib/db' +import { asc, desc } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' import { PageHeader } from '@/components/ui/Page' import { SubmitButton } from '@/components/ui' @@ -27,9 +28,9 @@ export const dynamic = 'force-dynamic' export default async function ComplaintsPage() { await requirePermission('complaints:read') - const complaints = await prisma.complaint.findMany({ - orderBy: [{ status: 'asc' }, { createdAt: 'desc' }], - select: { + const complaints = await db.query.complaint.findMany({ + orderBy: [asc(complaint.status), desc(complaint.createdAt)], + columns: { id: true, createdAt: true, subject: true, @@ -37,10 +38,12 @@ export default async function ComplaintsPage() { status: true, response: true, respondedAt: true, + }, + with: { // An anonymous complaint has no resident, and the null IS the anonymity — // there is nothing here to redact later because nothing was written. - resident: { select: { code: true, displayName: true } }, - respondedBy: { select: { name: true } }, + resident: { columns: { code: true, displayName: true } }, + respondedBy: { columns: { name: true } }, }, }) diff --git a/src/app/(admin)/events/page.tsx b/src/app/(admin)/events/page.tsx index e73140fe..a9f0cfac 100644 --- a/src/app/(admin)/events/page.tsx +++ b/src/app/(admin)/events/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit } from '@/lib/db' +import { asc } from 'drizzle-orm' import { EmptyState, ListShell, PageHeader, PageShell } from '@/components/ui/Page' import { EVENTS_ADMIN_LABELS } from '@/lib/constants' import { listStaffEvents, createEventAsStaff, cancelEvent } from '@/lib/actions/events' @@ -31,7 +32,10 @@ export default async function EventsAdminPage() { const canWriteEvents = !!staff && hasPermission(staff, 'events:write') const [events, units] = await Promise.all([ listStaffEvents(), - prisma.housingUnit.findMany({ select: { id: true, code: true }, orderBy: { code: 'asc' } }), + db.query.housingUnit.findMany({ + columns: { id: true, code: true }, + orderBy: [asc(housingUnit.code)], + }), ]) return ( diff --git a/src/app/(admin)/housing/[id]/edit/page.tsx b/src/app/(admin)/housing/[id]/edit/page.tsx index df0779ad..c6e0be16 100644 --- a/src/app/(admin)/housing/[id]/edit/page.tsx +++ b/src/app/(admin)/housing/[id]/edit/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit } from '@/lib/db' +import { eq } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { HousingFormFields, FormValidationUX } from '@/components/forms' @@ -23,8 +24,8 @@ export default async function EditHousingPage({ params }: Props) { await requirePermission('housing:write') const { id } = await params - const unit = await prisma.housingUnit.findUnique({ - where: { id }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, id), }) if (!unit) { diff --git a/src/app/(admin)/housing/[id]/page.tsx b/src/app/(admin)/housing/[id]/page.tsx index 0c0cf240..a3a0db17 100644 --- a/src/app/(admin)/housing/[id]/page.tsx +++ b/src/app/(admin)/housing/[id]/page.tsx @@ -1,5 +1,14 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { + db, + housingUnit, + placement, + placementSpot, + incident, + compatibilityAssessment, + resident as residentTable, +} from '@/lib/db' +import { eq, and, inArray, notInArray, asc, desc } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { @@ -37,7 +46,7 @@ import { calculateApartmentProfile, calculateApartmentFit } from '@/lib/compatib import { toResidentProfile } from '@/lib/compatibility/convert' import { getUnitFitConcerns } from '@/lib/compatibility' import { requirePermission } from '@/lib/auth' -import type { Resident, CompatibilityAssessment } from '@prisma/client' +import type { Resident, CompatibilityAssessment } from '@/lib/db' import type { ApartmentConflict } from '@/lib/compatibility/types' import type { HousingSpot } from '@/components/housing/types' import { hasResidentName, unitName, UNIT_NAME_SELECT } from '@/lib/utils/unit-name' @@ -58,9 +67,9 @@ export async function generateMetadata({ // Selects the nickname too, so the browser tab says what the residents call // the place. Selecting only `code` here is the exact under-selection that // UNIT_NAME_SELECT exists to prevent. - const unit = await prisma.housingUnit.findUnique({ - where: { id }, - select: UNIT_NAME_SELECT, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, id), + columns: UNIT_NAME_SELECT, }) return { title: unit ? unitName(unit) : 'Unterkunft' } } @@ -75,39 +84,39 @@ export default async function HousingDetailPage({ params }: Props) { await requirePermission('housing:read') const { id } = await params - const unit = await prisma.housingUnit.findUnique({ - where: { id }, - include: { + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, id), + with: { spots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, childSpots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(placementSpot.code)], }, placements: { - where: { status: 'ACTIVE' }, - include: { + where: eq(placement.status, 'ACTIVE'), + with: { resident: true, }, - orderBy: { startDate: 'desc' }, + orderBy: [desc(placement.startDate)], }, incidents: { - orderBy: { date: 'desc' }, - take: QUERY_LIMITS.unitHistory, - include: { - reportedBy: { select: RESIDENT_NAME_SELECT }, - subject: { select: RESIDENT_NAME_SELECT }, + orderBy: [desc(incident.date)], + limit: QUERY_LIMITS.unitHistory, + with: { + reportedBy: { columns: RESIDENT_NAME_SELECT }, + subject: { columns: RESIDENT_NAME_SELECT }, }, }, }, @@ -121,11 +130,11 @@ export default async function HousingDetailPage({ params }: Props) { const residentIds = unit.placements.map((p) => p.residentId) const compatibilityScores = residentIds.length > 1 - ? await prisma.compatibilityAssessment.findMany({ - where: { - residentId: { in: residentIds }, - comparedWithId: { in: residentIds }, - }, + ? await db.query.compatibilityAssessment.findMany({ + where: and( + inArray(compatibilityAssessment.residentId, residentIds), + inArray(compatibilityAssessment.comparedWithId, residentIds), + ), }) : [] @@ -179,12 +188,19 @@ export default async function HousingDetailPage({ params }: Props) { const hasAvailableSpace = unit.placements.length < unit.totalBeds if (hasAvailableSpace) { - // Get unplaced residents - const unplacedResidents = await prisma.resident.findMany({ - where: { - status: 'ACTIVE', - placements: { none: { status: 'ACTIVE' } }, - }, + // Get unplaced residents (no active placement — Prisma's `none` relation + // filter, expressed as a NOT IN subquery) + const unplacedResidents = await db.query.resident.findMany({ + where: and( + eq(residentTable.status, 'ACTIVE'), + notInArray( + residentTable.id, + db + .select({ id: placement.residentId }) + .from(placement) + .where(eq(placement.status, 'ACTIVE')), + ), + ), }) if (unplacedResidents.length > 0) { diff --git a/src/app/(admin)/housing/[id]/spots/page.tsx b/src/app/(admin)/housing/[id]/spots/page.tsx index 4f6f522f..fd2d75ba 100644 --- a/src/app/(admin)/housing/[id]/spots/page.tsx +++ b/src/app/(admin)/housing/[id]/spots/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit, placement, placementSpot } from '@/lib/db' +import { eq, asc } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' @@ -29,25 +30,25 @@ export default async function SpotManagementPage({ params, searchParams }: Props const { id } = await params const { new: isNewUnit } = await searchParams - const unit = await prisma.housingUnit.findUnique({ - where: { id }, - include: { + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, id), + with: { spots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, childSpots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(placementSpot.code)], }, }, }) diff --git a/src/app/(admin)/housing/page.tsx b/src/app/(admin)/housing/page.tsx index 48e619f5..4cbacc94 100644 --- a/src/app/(admin)/housing/page.tsx +++ b/src/app/(admin)/housing/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit, placement, incident, escapeLike } from '@/lib/db' +import { and, or, eq, inArray, ilike, gte, asc } from 'drizzle-orm' import { StatCard } from '@/components/ui/Card' import { getDateDaysAgo } from '@/lib/utils' import { requirePermission } from '@/lib/auth' @@ -37,28 +38,26 @@ export default async function HousingListPage({ searchParams }: Props) { // issued before the site axis existed. const unitFilter = unitScopeFilter(viewer) - const [units, allUnits] = await Promise.all([ - prisma.housingUnit.findMany({ - where: { - ...(unitFilter ?? {}), - ...(view === 'active' - ? { status: { in: ['AVAILABLE', 'FULL', 'MAINTENANCE'] } } + const [unitRows, allUnitRows] = await Promise.all([ + db.query.housingUnit.findMany({ + where: and( + unitFilter ?? undefined, + view === 'active' + ? inArray(housingUnit.status, ['AVAILABLE', 'FULL', 'MAINTENANCE']) : view === 'archived' - ? { status: 'CLOSED' } - : {}), - ...(q - ? { - OR: [ - { code: { contains: q, mode: 'insensitive' } }, - // A caseworker who hears "Casa Harmonie" must be able to type it. - { nickname: { contains: q, mode: 'insensitive' } }, - { address: { contains: q, mode: 'insensitive' } }, - { buildingCode: { contains: q, mode: 'insensitive' } }, - ], - } - : {}), - }, - select: { + ? eq(housingUnit.status, 'CLOSED') + : undefined, + q + ? or( + ilike(housingUnit.code, `%${escapeLike(q)}%`), + // A caseworker who hears "Casa Harmonie" must be able to type it. + ilike(housingUnit.nickname, `%${escapeLike(q)}%`), + ilike(housingUnit.address, `%${escapeLike(q)}%`), + ilike(housingUnit.buildingCode, `%${escapeLike(q)}%`), + ) + : undefined, + ), + columns: { id: true, code: true, // The name the residents gave their own home. Without it the staff @@ -70,39 +69,53 @@ export default async function HousingListPage({ searchParams }: Props) { totalBeds: true, totalRooms: true, wheelchairAccess: true, - _count: { - select: { - placements: { - where: { status: 'ACTIVE' }, - }, - incidents: { - where: { - date: { gte: getDateDaysAgo(30) }, - category: 'INTERPERSONAL', - }, - }, - }, + }, + with: { + placements: { + where: eq(placement.status, 'ACTIVE'), + columns: { id: true }, + }, + incidents: { + where: and( + gte(incident.date, getDateDaysAgo(30)), + eq(incident.category, 'INTERPERSONAL'), + ), + columns: { id: true }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }), // Tab counts and stats — unfiltered by VIEW (that is the point: the // counts describe every tab), but still scoped to the viewer's units. - prisma.housingUnit.findMany({ + db.query.housingUnit.findMany({ // Scoped as well: this feeds the view counts beside the tabs, and an // unscoped count tells a restricted viewer how many houses exist that // they cannot open. - where: { ...(unitFilter ?? {}) }, - select: { + where: unitFilter ?? undefined, + columns: { status: true, totalBeds: true, - _count: { - select: { placements: { where: { status: 'ACTIVE' } } }, + }, + with: { + placements: { + where: eq(placement.status, 'ACTIVE'), + columns: { id: true }, }, }, }), ]) + // Prisma's `_count` selects have no query-API equivalent — the filtered + // relations are fetched as id-only rows and counted here instead. + const units = unitRows.map(({ placements, incidents, ...rest }) => ({ + ...rest, + _count: { placements: placements.length, incidents: incidents.length }, + })) + const allUnits = allUnitRows.map(({ placements, ...rest }) => ({ + ...rest, + _count: { placements: placements.length }, + })) + const stats = { total: allUnits.length, available: allUnits.filter((u) => u.status === 'AVAILABLE').length, diff --git a/src/app/(admin)/incidents/[id]/page.tsx b/src/app/(admin)/incidents/[id]/page.tsx index 23028602..b657914e 100644 --- a/src/app/(admin)/incidents/[id]/page.tsx +++ b/src/app/(admin)/incidents/[id]/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +// `incident` is aliased: the fetched row below is also named `incident`. +import { db, incident as incidentTable, incidentFollowUp, conflictAgreement } from '@/lib/db' +import { eq, desc } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { clearFollowUpReminder } from '@/lib/actions' @@ -23,7 +25,10 @@ export async function generateMetadata({ params: Promise<{ id: string }> }): Promise { const { id } = await params - const incident = await prisma.incident.findUnique({ where: { id }, select: { type: true } }) + const incident = await db.query.incident.findFirst({ + where: eq(incidentTable.id, id), + columns: { type: true }, + }) return { title: incident ? (INCIDENT_TYPE_LABELS[incident.type as keyof typeof INCIDENT_TYPE_LABELS] ?? 'Vorfall') @@ -61,31 +66,31 @@ export default async function IncidentDetailPage({ params, searchParams }: Props const { id } = await params const sp = await searchParams - const incident = await prisma.incident.findUnique({ - where: { id }, - include: { + const incident = await db.query.incident.findFirst({ + where: eq(incidentTable.id, id), + with: { housingUnit: { - select: { code: true, address: true }, + columns: { code: true, address: true }, }, reportedBy: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, subject: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, involvedResidents: { - include: { + with: { resident: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, }, }, followUps: { - orderBy: { createdAt: 'desc' }, + orderBy: [desc(incidentFollowUp.createdAt)], }, agreements: { - orderBy: { createdAt: 'desc' }, - include: { parties: { include: { resident: { select: RESIDENT_NAME_SELECT } } } }, + orderBy: [desc(conflictAgreement.createdAt)], + with: { parties: { with: { resident: { columns: RESIDENT_NAME_SELECT } } } }, }, }, }) diff --git a/src/app/(admin)/incidents/new/page.tsx b/src/app/(admin)/incidents/new/page.tsx index e35be3e7..2bdc5471 100644 --- a/src/app/(admin)/incidents/new/page.tsx +++ b/src/app/(admin)/incidents/new/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import Link from 'next/link' -import { prisma } from '@/lib/db' +import { db, housingUnit, resident } from '@/lib/db' +import { ne, inArray, asc } from 'drizzle-orm' import { createIncident } from '@/lib/actions' import { requirePermission } from '@/lib/auth' @@ -39,13 +40,13 @@ export default async function NewIncidentPage({ searchParams }: Props) { const params = await searchParams const [units, residents] = await Promise.all([ - prisma.housingUnit.findMany({ - where: { status: { not: 'CLOSED' } }, - orderBy: { code: 'asc' }, + db.query.housingUnit.findMany({ + where: ne(housingUnit.status, 'CLOSED'), + orderBy: [asc(housingUnit.code)], }), - prisma.resident.findMany({ - where: { status: { in: ['ACTIVE', 'PLACED'] } }, - orderBy: { code: 'asc' }, + db.query.resident.findMany({ + where: inArray(resident.status, ['ACTIVE', 'PLACED']), + orderBy: [asc(resident.code)], }), ]) diff --git a/src/app/(admin)/incidents/page.tsx b/src/app/(admin)/incidents/page.tsx index c14ffd61..52533de7 100644 --- a/src/app/(admin)/incidents/page.tsx +++ b/src/app/(admin)/incidents/page.tsx @@ -1,5 +1,7 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +// `incident` is aliased: the row-mapping callbacks below use the same name. +import { db, incident as incidentTable } from '@/lib/db' +import { eq, and, isNull, isNotNull, desc, count } from 'drizzle-orm' import Link from 'next/link' import { X, AlertTriangle, Home, Megaphone, User, Clock, Timer } from 'lucide-react' import { @@ -18,7 +20,7 @@ import { getSeverityBorderClass, getSeverityDotClass, formatRelativeDate } from import { StatCard } from '@/components/ui/Card' import { TabLink, TabLinkGroup } from '@/components/ui/Tabs' import { PageHeader } from '@/components/ui/Page' -import type { IncidentCategory, Prisma } from '@prisma/client' +import type { IncidentCategory } from '@/lib/db' import { QUERY_LIMITS } from '@/lib/config/thresholds' import { RESIDENT_NAME_SELECT, residentName, type NamedResident } from '@/lib/utils/resident-name' import { requirePermission } from '@/lib/auth' @@ -48,16 +50,18 @@ export default async function IncidentsListPage({ searchParams }: Props) { const canWriteIncidents = hasPermission(viewer, 'incidents:write') const canExport = hasPermission(viewer, 'export:read') - const where: Prisma.IncidentWhereInput = { - ...(categoryFilter !== 'all' ? { category: categoryFilter as IncidentCategory } : {}), - ...(statusFilter === 'open' ? { resolvedAt: null } : {}), - ...(statusFilter === 'resolved' ? { resolvedAt: { not: null } } : {}), - } + const conditions = [ + ...(categoryFilter !== 'all' + ? [eq(incidentTable.category, categoryFilter as IncidentCategory)] + : []), + ...(statusFilter === 'open' ? [isNull(incidentTable.resolvedAt)] : []), + ...(statusFilter === 'resolved' ? [isNotNull(incidentTable.resolvedAt)] : []), + ] - const [incidents, categoryGroups, openCategoryGroups, criticalOpenCount] = await Promise.all([ - prisma.incident.findMany({ - where: Object.keys(where).length > 0 ? where : undefined, - select: { + const [incidentRows, categoryGroups, openCategoryGroups, criticalOpenCount] = await Promise.all([ + db.query.incident.findMany({ + where: conditions.length > 0 ? and(...conditions) : undefined, + columns: { id: true, type: true, category: true, @@ -68,51 +72,60 @@ export default async function IncidentsListPage({ searchParams }: Props) { resolution: true, nextFollowUpDate: true, mediationMinutes: true, + }, + with: { housingUnit: { - select: { code: true }, + columns: { code: true }, }, reportedBy: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, subject: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, - _count: { - select: { followUps: true }, + // Prisma's `_count.followUps`: fetched id-only and counted below. + followUps: { + columns: { id: true }, }, }, - orderBy: { date: 'desc' }, - take: QUERY_LIMITS.pageList, + orderBy: [desc(incidentTable.date)], + limit: QUERY_LIMITS.pageList, }), // Total counts per category (single query instead of fetching all rows) - prisma.incident.groupBy({ - by: ['category'], - _count: { _all: true }, - }), + db + .select({ category: incidentTable.category, count: count() }) + .from(incidentTable) + .groupBy(incidentTable.category), // Open (resolvedAt = null) counts per category - prisma.incident.groupBy({ - by: ['category'], - where: { resolvedAt: null }, - _count: { _all: true }, - }), + db + .select({ category: incidentTable.category, count: count() }) + .from(incidentTable) + .where(isNull(incidentTable.resolvedAt)) + .groupBy(incidentTable.category), // Count of critical, still-open incidents - prisma.incident.count({ - where: { severity: 'CRITICAL', resolvedAt: null }, - }), + db.$count( + incidentTable, + and(eq(incidentTable.severity, 'CRITICAL'), isNull(incidentTable.resolvedAt)), + ), ]) + const incidents = incidentRows.map(({ followUps, ...rest }) => ({ + ...rest, + _count: { followUps: followUps.length }, + })) + const categoryCounts = categoryGroups.reduce>((acc, g) => { - acc[g.category] = g._count._all + acc[g.category] = g.count return acc }, {}) const openCategoryCounts = openCategoryGroups.reduce>((acc, g) => { - acc[g.category] = g._count._all + acc[g.category] = g.count return acc }, {}) const stats = { - total: categoryGroups.reduce((sum, g) => sum + g._count._all, 0), - open: openCategoryGroups.reduce((sum, g) => sum + g._count._all, 0), + total: categoryGroups.reduce((sum, g) => sum + g.count, 0), + open: openCategoryGroups.reduce((sum, g) => sum + g.count, 0), interpersonal: categoryCounts.INTERPERSONAL ?? 0, openInterpersonal: openCategoryCounts.INTERPERSONAL ?? 0, maintenance: categoryCounts.MAINTENANCE ?? 0, diff --git a/src/app/(admin)/maintenance/[id]/page.tsx b/src/app/(admin)/maintenance/[id]/page.tsx index 5bb7a14c..1e5981e5 100644 --- a/src/app/(admin)/maintenance/[id]/page.tsx +++ b/src/app/(admin)/maintenance/[id]/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, maintenanceRequest } from '@/lib/db' +import { eq } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { updateMaintenanceStatus } from '@/lib/actions' @@ -25,9 +26,9 @@ export async function generateMetadata({ params: Promise<{ id: string }> }): Promise { const { id } = await params - const request = await prisma.maintenanceRequest.findUnique({ - where: { id }, - select: { category: true }, + const request = await db.query.maintenanceRequest.findFirst({ + where: eq(maintenanceRequest.id, id), + columns: { category: true }, }) return { title: request @@ -49,12 +50,12 @@ export default async function MaintenanceDetailPage({ params }: Props) { await requirePermission('maintenance:read') const { id } = await params - const request = await prisma.maintenanceRequest.findUnique({ - where: { id }, - include: { + const request = await db.query.maintenanceRequest.findFirst({ + where: eq(maintenanceRequest.id, id), + with: { housingUnit: true, spot: true, - reportedBy: { select: RESIDENT_NAME_SELECT }, + reportedBy: { columns: RESIDENT_NAME_SELECT }, }, }) diff --git a/src/app/(admin)/maintenance/new/page.tsx b/src/app/(admin)/maintenance/new/page.tsx index 1302a225..0269601c 100644 --- a/src/app/(admin)/maintenance/new/page.tsx +++ b/src/app/(admin)/maintenance/new/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, housingUnit, placementSpot, resident } from '@/lib/db' +import { ne, inArray, asc } from 'drizzle-orm' import Link from 'next/link' import { createMaintenanceRequest } from '@/lib/actions' import { requirePermission } from '@/lib/auth' @@ -31,20 +32,20 @@ export default async function NewMaintenanceRequestPage({ searchParams }: Props) const preselectedSpotId = params.spot const [housingUnits, residents] = await Promise.all([ - prisma.housingUnit.findMany({ - where: { status: { not: 'CLOSED' } }, - include: { + db.query.housingUnit.findMany({ + where: ne(housingUnit.status, 'CLOSED'), + with: { spots: { - where: { type: { not: 'ROOM' } }, - orderBy: { code: 'asc' }, + where: ne(placementSpot.type, 'ROOM'), + orderBy: [asc(placementSpot.code)], }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }), - prisma.resident.findMany({ - where: { status: { in: ['ACTIVE', 'PLACED'] } }, - select: RESIDENT_NAME_SELECT, - orderBy: { code: 'asc' }, + db.query.resident.findMany({ + where: inArray(resident.status, ['ACTIVE', 'PLACED']), + columns: RESIDENT_NAME_SELECT, + orderBy: [asc(resident.code)], }), ]) diff --git a/src/app/(admin)/maintenance/page.tsx b/src/app/(admin)/maintenance/page.tsx index 9ec4e7d4..dd629a9c 100644 --- a/src/app/(admin)/maintenance/page.tsx +++ b/src/app/(admin)/maintenance/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, maintenanceRequest } from '@/lib/db' +import { eq, and, inArray, notInArray, asc, desc, count, type SQL } from 'drizzle-orm' import Link from 'next/link' import { updateMaintenanceStatus, assignMaintenanceRequest } from '@/lib/actions' import { requirePermission } from '@/lib/auth' @@ -20,7 +21,7 @@ import { formatRelativeDate } from '@/lib/utils' import { StatCard } from '@/components/ui/Card' import { TabLink, TabLinkGroup } from '@/components/ui/Tabs' import { PageHeader } from '@/components/ui/Page' -import type { MaintenanceStatus, Prisma } from '@prisma/client' +import type { MaintenanceStatus } from '@/lib/db' import { QUERY_LIMITS } from '@/lib/config/thresholds' import { RESIDENT_NAME_SELECT, residentName, type NamedResident } from '@/lib/utils/resident-name' @@ -55,17 +56,17 @@ export default async function MaintenancePage({ searchParams }: Props) { const statusFilter = params.status || 'active' // Build where clause based on status filter - let whereClause: Prisma.MaintenanceRequestWhereInput = {} + let whereClause: SQL | undefined if (statusFilter === 'active') { - whereClause.status = { in: ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD'] } + whereClause = inArray(maintenanceRequest.status, ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD']) } else if (statusFilter !== 'all') { - whereClause.status = statusFilter as MaintenanceStatus + whereClause = eq(maintenanceRequest.status, statusFilter as MaintenanceStatus) } const [requests, statusGroups, urgentActiveCount] = await Promise.all([ - prisma.maintenanceRequest.findMany({ + db.query.maintenanceRequest.findMany({ where: whereClause, - select: { + columns: { id: true, title: true, description: true, @@ -77,38 +78,41 @@ export default async function MaintenancePage({ searchParams }: Props) { createdAt: true, housingUnitId: true, reportedById: true, + }, + with: { housingUnit: { - select: { code: true }, + columns: { code: true }, }, spot: { - select: { code: true, label: true }, + columns: { code: true, label: true }, }, reportedBy: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, }, - orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }], - take: QUERY_LIMITS.pageList, + orderBy: [asc(maintenanceRequest.priority), desc(maintenanceRequest.createdAt)], + limit: QUERY_LIMITS.pageList, }), // Aggregate tab counts by status (single query instead of fetching all rows) - prisma.maintenanceRequest.groupBy({ - by: ['status'], - _count: { _all: true }, - }), + db + .select({ status: maintenanceRequest.status, count: count() }) + .from(maintenanceRequest) + .groupBy(maintenanceRequest.status), // Urgent + still-active requests (separate query — combines priority + status NOT IN) - prisma.maintenanceRequest.count({ - where: { - priority: 'URGENT', - status: { notIn: ['COMPLETED', 'CANCELLED'] }, - }, - }), + db.$count( + maintenanceRequest, + and( + eq(maintenanceRequest.priority, 'URGENT'), + notInArray(maintenanceRequest.status, ['COMPLETED', 'CANCELLED']), + ), + ), ]) const statusCounts = statusGroups.reduce>((acc, g) => { - acc[g.status] = g._count._all + acc[g.status] = g.count return acc }, {}) - const totalRequests = statusGroups.reduce((sum, g) => sum + g._count._all, 0) + const totalRequests = statusGroups.reduce((sum, g) => sum + g.count, 0) const activeCount = totalRequests - (statusCounts.COMPLETED ?? 0) - (statusCounts.CANCELLED ?? 0) const stats = { diff --git a/src/app/(admin)/matching/page.tsx b/src/app/(admin)/matching/page.tsx index d01a42e9..a66fdd85 100644 --- a/src/app/(admin)/matching/page.tsx +++ b/src/app/(admin)/matching/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, resident, placement, housingUnit } from '@/lib/db' +import { eq, and, inArray, notInArray, desc, asc } from 'drizzle-orm' import { logger } from '@/lib/logger' export const metadata: Metadata = { title: 'Matching' } @@ -13,7 +14,7 @@ import { bestRoomFit } from '@/lib/compatibility/room-fit' import { validateScoreForDiscrimination } from '@/lib/compatibility/safeguards' import type { SafeguardWarning } from '@/lib/compatibility/safeguards' import { calculateUnitMetrics, getSimilarPlacementSuccessRate } from '@/lib/analytics/unit-metrics' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ApartmentConflict } from '@/lib/compatibility/types' import type { MatchResult, @@ -52,47 +53,60 @@ export default async function MatchingPage({ searchParams }: Props) { number, MatchUnit[], ] = await Promise.all([ - prisma.resident.findMany({ - where: { - status: 'ACTIVE', - placements: { none: { status: 'ACTIVE' } }, - }, - orderBy: { createdAt: 'desc' }, + db.query.resident.findMany({ + // `placements: { none: { status: ACTIVE } }` — residents without an + // active placement, expressed as a NOT IN subquery. + where: and( + eq(resident.status, 'ACTIVE'), + notInArray( + resident.id, + db + .select({ residentId: placement.residentId }) + .from(placement) + .where(eq(placement.status, 'ACTIVE')), + ), + ), + orderBy: [desc(resident.createdAt)], }), - prisma.resident.findMany({ - where: { - status: 'PLACED', - placements: { some: { status: 'ACTIVE' } }, - }, - include: { + db.query.resident.findMany({ + // `placements: { some: { status: ACTIVE } }` — as an IN subquery. + where: and( + eq(resident.status, 'PLACED'), + inArray( + resident.id, + db + .select({ residentId: placement.residentId }) + .from(placement) + .where(eq(placement.status, 'ACTIVE')), + ), + ), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { housingUnit: { select: { id: true, code: true } } }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + with: { housingUnit: { columns: { id: true, code: true } } }, + limit: 1, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(resident.code)], }), - prisma.resident.count(), - prisma.housingUnit.findMany({ - where: { - status: { in: ['AVAILABLE', 'FULL'] }, - }, - include: { + db.$count(resident), + db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, spots: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }), ]) @@ -138,17 +152,17 @@ export default async function MatchingPage({ searchParams }: Props) { } if (params.resident) { - const foundResident = await prisma.resident.findUnique({ - where: { id: params.resident }, - include: { + const foundResident = await db.query.resident.findFirst({ + where: eq(resident.id, params.resident), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { housingUnit: { select: { id: true, code: true } } }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + with: { housingUnit: { columns: { id: true, code: true } } }, + limit: 1, }, }, }) - selectedResident = foundResident + selectedResident = foundResident ?? null if (foundResident) { const filteredUnits = availableUnits.filter((unit) => unit.placements.length < unit.totalBeds) diff --git a/src/app/(admin)/messages/[residentId]/page.tsx b/src/app/(admin)/messages/[residentId]/page.tsx index 6f509b9a..f49dd6c9 100644 --- a/src/app/(admin)/messages/[residentId]/page.tsx +++ b/src/app/(admin)/messages/[residentId]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { notFound } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { PageHeader } from '@/components/ui/Page' import { StaffReplyThread } from '@/components/messaging/StaffReplyThread' import { getOrCreateThread, loadThreadMessages, markThreadRead } from '@/lib/messaging/queries' @@ -20,9 +21,9 @@ export default async function StaffThreadPage({ const { residentId } = await params const staff = await requireStaffAuth() - const resident = await prisma.resident.findUnique({ - where: { id: residentId }, - select: RESIDENT_NAME_SELECT, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + columns: RESIDENT_NAME_SELECT, }) if (!resident) notFound() diff --git a/src/app/(admin)/page.tsx b/src/app/(admin)/page.tsx index feae2da5..0280e45c 100644 --- a/src/app/(admin)/page.tsx +++ b/src/app/(admin)/page.tsx @@ -1,5 +1,19 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { + db, + resident, + housingUnit, + placement, + incident, + maintenanceRequest, + transferRequest, + learningRecord, + houseEvent, + user as userTable, + careAssignment, + satisfactionCheckIn, +} from '@/lib/db' +import { eq, and, gte, inArray, isNull, desc, asc } from 'drizzle-orm' import { daysSinceCeil, getDateDaysAgo } from '@/lib/utils' export const metadata: Metadata = { title: 'Dashboard' } @@ -78,48 +92,48 @@ export default async function AdminDashboard() { neverSignedInStaffCount, assignedResidentCount, ] = await Promise.all([ - prisma.resident.count(), + db.$count(resident), // Only used to pick the first setup step, which requires housing:write — // a subset of the housing:read this section is gated on. - show('occupancy') ? prisma.housingUnit.count() : 0, + show('occupancy') ? db.$count(housingUnit) : 0, show('matching') - ? prisma.resident.findMany({ - where: { status: { in: ['ACTIVE', 'PLACED'] } }, - select: { ...RESIDENT_NAME_SELECT, status: true, createdAt: true }, + ? db.query.resident.findMany({ + where: inArray(resident.status, ['ACTIVE', 'PLACED']), + columns: { ...RESIDENT_NAME_SELECT, status: true, createdAt: true }, }) : [], show('occupancy') - ? prisma.housingUnit.findMany({ - select: { totalBeds: true, status: true }, + ? db.query.housingUnit.findMany({ + columns: { totalBeds: true, status: true }, }) : [], - show('occupancy') ? prisma.placement.count({ where: { status: 'ACTIVE' } }) : 0, + show('occupancy') ? db.$count(placement, eq(placement.status, 'ACTIVE')) : 0, show('checkIns') - ? prisma.placement.findMany({ - where: { status: 'ACTIVE' }, - select: { + ? db.query.placement.findMany({ + where: eq(placement.status, 'ACTIVE'), + columns: { id: true, startDate: true, + }, + with: { resident: { - select: { ...RESIDENT_NAME_SELECT, supportLevel: true }, + columns: { ...RESIDENT_NAME_SELECT, supportLevel: true }, }, housingUnit: { - select: { code: true }, + columns: { code: true }, }, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, - select: { createdAt: true }, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, + columns: { createdAt: true }, }, }, }) : [], show('incidents') - ? prisma.incident.findMany({ - where: { - date: { gte: getDateDaysAgo(PROBLEM_DETECTION.recentIncidentsDays) }, - }, - select: { + ? db.query.incident.findMany({ + where: gte(incident.date, getDateDaysAgo(PROBLEM_DETECTION.recentIncidentsDays)), + columns: { id: true, type: true, category: true, @@ -127,52 +141,60 @@ export default async function AdminDashboard() { date: true, resolvedAt: true, housingUnitId: true, - housingUnit: { select: { code: true } }, }, - orderBy: { date: 'desc' }, + with: { + housingUnit: { columns: { code: true } }, + }, + orderBy: [desc(incident.date)], }) : [], show('maintenance') - ? prisma.maintenanceRequest.count({ - where: { - status: { in: ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD'] }, - }, - }) + ? db.$count( + maintenanceRequest, + inArray(maintenanceRequest.status, ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD']), + ) : 0, show('transferRequests') - ? prisma.transferRequest.findMany({ - where: { status: 'PENDING' }, - select: { + ? db.query.transferRequest.findMany({ + where: eq(transferRequest.status, 'PENDING'), + columns: { id: true, createdAt: true, - resident: { select: RESIDENT_NAME_SELECT }, + }, + with: { + resident: { columns: RESIDENT_NAME_SELECT }, currentPlacement: { - select: { housingUnit: { select: { code: true } } }, + columns: {}, + with: { housingUnit: { columns: { code: true } } }, }, }, - orderBy: { createdAt: 'asc' }, + orderBy: [asc(transferRequest.createdAt)], }) : [], show('proposals') ? getProposalsAwaitingStaff() : [], - show('learning') ? prisma.learningRecord.count({ where: { status: 'IN_PROGRESS' } }) : 0, + show('learning') ? db.$count(learningRecord, eq(learningRecord.status, 'IN_PROGRESS')) : 0, show('learning') - ? prisma.learningRecord.count({ - where: { - status: 'COMPLETED', - completedAt: { gte: getDateDaysAgo(LEARNING_PULSE_WINDOW_DAYS) }, - }, - }) + ? db.$count( + learningRecord, + and( + eq(learningRecord.status, 'COMPLETED'), + gte(learningRecord.completedAt, getDateDaysAgo(LEARNING_PULSE_WINDOW_DAYS)), + ), + ) : 0, show('events') - ? prisma.houseEvent.count({ - where: { status: 'PUBLISHED', startsAt: { gte: now } }, - }) + ? db.$count( + houseEvent, + and(eq(houseEvent.status, 'PUBLISHED'), gte(houseEvent.startsAt, now)), + ) : 0, - show('team') ? prisma.user.count({ where: { active: true } }) : 0, + show('team') ? db.$count(userTable, eq(userTable.active, true)) : 0, // Provisioned and never used. A staff code that was issued but never // signed in with is invisible everywhere else in the product — it is not // an error, it is an unfinished handover, and only Leitung can close it. - show('team') ? prisma.user.count({ where: { active: true, lastLoginAt: null } }) : 0, + show('team') + ? db.$count(userTable, and(eq(userTable.active, true), isNull(userTable.lastLoginAt))) + : 0, // How many clients sit in THIS person's care seat. // // null for a viewer whose reach is every domain: they have no single seat @@ -182,7 +204,7 @@ export default async function AdminDashboard() { // cannot tell those apart, and reported the second as the first. viewer.scope === 'ALL_DOMAINS' || !user ? null - : prisma.careAssignment.count({ where: { staffId: user.id } }), + : db.$count(careAssignment, eq(careAssignment.staffId, user.id)), ]) // ============================================================================= diff --git a/src/app/(admin)/placements/[id]/checkin/page.tsx b/src/app/(admin)/placements/[id]/checkin/page.tsx index c91fdc0f..bd371fbd 100644 --- a/src/app/(admin)/placements/[id]/checkin/page.tsx +++ b/src/app/(admin)/placements/[id]/checkin/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, placement as placementTable, satisfactionCheckIn } from '@/lib/db' +import { eq, desc } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { createCheckInFromForm } from '@/lib/actions' @@ -30,15 +31,15 @@ export default async function NewCheckInPage({ params, searchParams }: Props) { const sp = await searchParams // Get placement with resident and housing info - const placement = await prisma.placement.findUnique({ - where: { id }, - include: { + const placement = await db.query.placement.findFirst({ + where: eq(placementTable.id, id), + with: { resident: true, housingUnit: true, spot: true, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 5, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 5, }, }, }) diff --git a/src/app/(admin)/placements/page.tsx b/src/app/(admin)/placements/page.tsx index 23bc3efd..7aab6288 100644 --- a/src/app/(admin)/placements/page.tsx +++ b/src/app/(admin)/placements/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, placement, satisfactionCheckIn } from '@/lib/db' +import { eq, ne, isNotNull, desc, count, avg } from 'drizzle-orm' import Link from 'next/link' import { PLACEMENT_STATUS_LABELS, @@ -45,14 +46,14 @@ export default async function PlacementsListPage({ searchParams }: Props) { const conflictsOnly = params.conflicts === '1' const [placements, statusGroups, avgSatisfactionAgg, conflictEndsCount] = await Promise.all([ - prisma.placement.findMany({ + db.query.placement.findMany({ where: statusFilter === 'active' - ? { status: 'ACTIVE' } + ? eq(placement.status, 'ACTIVE') : statusFilter === 'ended' - ? { status: { not: 'ACTIVE' } } + ? ne(placement.status, 'ACTIVE') : undefined, - select: { + columns: { id: true, status: true, startDate: true, @@ -61,56 +62,56 @@ export default async function PlacementsListPage({ searchParams }: Props) { endReason: true, residentId: true, housingUnitId: true, + }, + with: { resident: { - select: { ...RESIDENT_NAME_SELECT, supportLevel: true }, + columns: { ...RESIDENT_NAME_SELECT, supportLevel: true }, }, housingUnit: { - select: { code: true, address: true }, + columns: { code: true, address: true }, }, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, - select: { + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, + columns: { createdAt: true, overallSatisfaction: true, concerns: true, }, }, }, - orderBy: { startDate: 'desc' }, - take: 200, + orderBy: [desc(placement.startDate)], + limit: 200, }), // Aggregate tab counts by status (single query instead of fetching all rows) - prisma.placement.groupBy({ - by: ['status'], - _count: { _all: true }, - }), + db + .select({ status: placement.status, count: count() }) + .from(placement) + .groupBy(placement.status), // Average satisfaction across all placements with a rating set - prisma.placement.aggregate({ - where: { satisfactionRating: { not: null } }, - _avg: { satisfactionRating: true }, - }), + db + .select({ avg: avg(placement.satisfactionRating) }) + .from(placement) + .where(isNotNull(placement.satisfactionRating)), // Count of placements that ended due to conflict - prisma.placement.count({ - where: { endReason: 'CONFLICT' }, - }), + db.$count(placement, eq(placement.endReason, 'CONFLICT')), ]) const statusCounts = statusGroups.reduce>((acc, g) => { - acc[g.status] = g._count._all + acc[g.status] = g.count return acc }, {}) - const totalPlacements = statusGroups.reduce((sum, g) => sum + g._count._all, 0) + const totalPlacements = statusGroups.reduce((sum, g) => sum + g.count, 0) const activeCount = statusCounts.ACTIVE ?? 0 + // avg() comes back as string | null from node-postgres (Prisma returned number | null) + const avgSatisfactionRaw = avgSatisfactionAgg[0]?.avg ?? null + const stats = { total: totalPlacements, active: activeCount, ended: totalPlacements - activeCount, - avgSatisfaction: - avgSatisfactionAgg._avg.satisfactionRating !== null - ? Math.round(avgSatisfactionAgg._avg.satisfactionRating) - : null, + avgSatisfaction: avgSatisfactionRaw !== null ? Math.round(Number(avgSatisfactionRaw)) : null, conflictEnds: conflictEndsCount, } diff --git a/src/app/(admin)/residents/[id]/edit/page.tsx b/src/app/(admin)/residents/[id]/edit/page.tsx index b64b223a..77273536 100644 --- a/src/app/(admin)/residents/[id]/edit/page.tsx +++ b/src/app/(admin)/residents/[id]/edit/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { ResidentFormFields, FormValidationUX } from '@/components/forms' @@ -23,8 +24,8 @@ export default async function EditResidentPage({ params }: Props) { await requirePermission('residents:write') const { id } = await params - const resident = await prisma.resident.findUnique({ - where: { id }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, id), }) if (!resident) { diff --git a/src/app/(admin)/residents/[id]/page.tsx b/src/app/(admin)/residents/[id]/page.tsx index 1e3bb0fa..d7d8d2f7 100644 --- a/src/app/(admin)/residents/[id]/page.tsx +++ b/src/app/(admin)/residents/[id]/page.tsx @@ -1,6 +1,16 @@ import type { Metadata } from 'next' -import type { HousingUnit, Resident } from '@prisma/client' -import { prisma } from '@/lib/db' +import type { HousingUnit, Resident } from '@/lib/db' +import { + db, + resident as residentTable, + placement, + placementSpot, + incident, + learningRecord, + compatibilityAssessment, + housingUnit, +} from '@/lib/db' +import { eq, ne, and, inArray, notInArray, desc, asc } from 'drizzle-orm' import { notFound } from 'next/navigation' import Link from 'next/link' import { SPOT_TYPE_ICONS } from '@/lib/config/placement-spots' @@ -56,9 +66,9 @@ export async function generateMetadata({ params: Promise<{ id: string }> }): Promise { const { id } = await params - const resident = await prisma.resident.findUnique({ - where: { id }, - select: { code: true, displayName: true }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, id), + columns: { code: true, displayName: true }, }) return { title: resident ? residentName(resident) : 'Klient*in' } } @@ -94,60 +104,58 @@ export default async function ResidentDetailPage({ params, searchParams }: Props careAppointments, documents, ] = await Promise.all([ - prisma.resident.findUnique({ - where: { id }, - include: { + db.query.resident.findFirst({ + where: eq(residentTable.id, id), + with: { placements: { - include: { + with: { housingUnit: true, spot: true, }, - orderBy: { startDate: 'desc' }, + orderBy: [desc(placement.startDate)], }, - learningRecords: { orderBy: { updatedAt: 'desc' } }, + learningRecords: { orderBy: [desc(learningRecord.updatedAt)] }, incidentsAsSubject: { - include: { + with: { housingUnit: true, }, - orderBy: { date: 'desc' }, - take: QUERY_LIMITS.residentHistory, + orderBy: [desc(incident.date)], + limit: QUERY_LIMITS.residentHistory, }, incidentsReported: { - include: { + with: { housingUnit: true, }, - orderBy: { date: 'desc' }, - take: QUERY_LIMITS.residentHistory, + orderBy: [desc(incident.date)], + limit: QUERY_LIMITS.residentHistory, }, assessments: { - include: { + with: { comparedWith: true, }, - orderBy: { overallScore: 'desc' }, - take: 5, + orderBy: [desc(compatibilityAssessment.overallScore)], + limit: 5, }, }, }), // Available units for placement/transfer actions canWritePlacements - ? prisma.housingUnit.findMany({ - where: { - status: { in: ['AVAILABLE', 'FULL'] }, - }, - include: { + ? db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + with: { spots: { - where: { - status: 'AVAILABLE', - type: { not: 'ROOM' }, // Only assignable spots - }, - orderBy: { code: 'asc' }, + where: and( + eq(placementSpot.status, 'AVAILABLE'), + ne(placementSpot.type, 'ROOM'), // Only assignable spots + ), + orderBy: [asc(placementSpot.code)], }, placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }) : Promise.resolve([]), getCareTeam(id), @@ -238,22 +246,30 @@ export default async function ResidentDetailPage({ params, searchParams }: Props if (!currentPlacement && canWritePlacements) { // Both queries depend only on resident.id — fetch in parallel const [unitsWithResidents, otherUnplaced] = await Promise.all([ - prisma.housingUnit.findMany({ - where: { status: { in: ['AVAILABLE', 'FULL'] } }, - include: { + db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, - spots: { where: { status: 'AVAILABLE' } }, + spots: { where: eq(placementSpot.status, 'AVAILABLE') }, }, }), - prisma.resident.findMany({ - where: { - id: { not: resident.id }, - status: 'ACTIVE', - placements: { none: { status: 'ACTIVE' } }, - }, + db.query.resident.findMany({ + // `placements: { none: { status: ACTIVE } }` — no active placement, + // expressed as a NOT IN subquery. + where: and( + ne(residentTable.id, resident.id), + eq(residentTable.status, 'ACTIVE'), + notInArray( + residentTable.id, + db + .select({ residentId: placement.residentId }) + .from(placement) + .where(eq(placement.status, 'ACTIVE')), + ), + ), }), ]) diff --git a/src/app/(admin)/residents/page.tsx b/src/app/(admin)/residents/page.tsx index bc2d288d..2e11f40c 100644 --- a/src/app/(admin)/residents/page.tsx +++ b/src/app/(admin)/residents/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { db, escapeLike, resident, placement, incident, satisfactionCheckIn } from '@/lib/db' +import { eq, and, or, gte, inArray, notInArray, isNotNull, ilike, desc, count } from 'drizzle-orm' import { EMPTY_STATE_LABELS, RESIDENT_LIST_LABELS, @@ -67,7 +68,7 @@ export default async function ResidentsListPage({ searchParams }: Props) { const can = (permission: StaffPermission) => hasPermission(viewer, permission) // Which PLACES this viewer covers. Null for ALL_UNITS — everyone, until - // somebody is deliberately narrowed — so the spread below adds nothing and + // somebody is deliberately narrowed — so the `and()` below adds nothing and // the common query is unchanged. // // Read from `currentUser`, not from `viewer`: the NARROWEST_CAPABILITIES @@ -76,89 +77,106 @@ export default async function ResidentsListPage({ searchParams }: Props) { // with sites. const siteFilter = currentUser ? residentScopeFilter(currentUser) : null - const [residents, statusGroups, unplacedCount, myResidentIds] = await Promise.all([ - prisma.resident.findMany({ - where: { - ...(siteFilter ?? {}), - ...(view === 'active' - ? { status: { in: ['ACTIVE', 'PLACED'] } } - : view === 'archived' - ? { status: 'EXITED' } - : {}), - ...(q - ? { - OR: [ - { code: { contains: q, mode: 'insensitive' } }, - { displayName: { contains: q, mode: 'insensitive' } }, - ], - } - : {}), - }, - select: { - ...RESIDENT_NAME_SELECT, - ageRange: true, - gender: true, - status: true, - supportLevel: true, - languages: true, - createdAt: true, - placements: { - where: { status: 'ACTIVE' }, - select: { - startDate: true, - housingUnit: { select: { code: true } }, - checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, - select: { createdAt: true }, - }, - }, - }, - careAssignments: { - select: { - role: true, - staff: { select: { name: true } }, - }, - }, - careAttributes: { - select: { key: true, value: true, domain: true }, + const residentsWhere = and( + siteFilter ?? undefined, + view === 'active' + ? inArray(resident.status, ['ACTIVE', 'PLACED']) + : view === 'archived' + ? eq(resident.status, 'EXITED') + : undefined, + q + ? or( + ilike(resident.code, `%${escapeLike(q)}%`), + ilike(resident.displayName, `%${escapeLike(q)}%`), + ) + : undefined, + ) + + const [residents, statusGroups, unplacedCount, myResidentIds, incidentGroups] = await Promise.all( + [ + db.query.resident.findMany({ + where: residentsWhere, + columns: { + ...RESIDENT_NAME_SELECT, + ageRange: true, + gender: true, + status: true, + supportLevel: true, + languages: true, + createdAt: true, }, - _count: { - select: { - incidentsAsSubject: { - where: { - date: { gte: getDateDaysAgo(30) }, - category: 'INTERPERSONAL', + with: { + placements: { + where: eq(placement.status, 'ACTIVE'), + columns: { startDate: true }, + with: { + housingUnit: { columns: { code: true } }, + checkIns: { + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, + columns: { createdAt: true }, }, }, }, + careAssignments: { + columns: { role: true }, + with: { + staff: { columns: { name: true } }, + }, + }, + careAttributes: { + columns: { key: true, value: true, domain: true }, + }, }, - }, - orderBy: { createdAt: 'desc' }, - }), - // Aggregate tab counts by status (single query instead of fetching all rows) - prisma.resident.groupBy({ - by: ['status'], - _count: { _all: true }, - }), - // Count of ACTIVE residents with no active placement (separate query) - prisma.resident.count({ - where: { - status: 'ACTIVE', - placements: { none: { status: 'ACTIVE' } }, - }, - }), - // "My clients" — IDs where this user is a care worker - currentUser ? getMyResidentIds(currentUser.id) : Promise.resolve([]), - ]) + orderBy: [desc(resident.createdAt)], + }), + // Aggregate tab counts by status (single query instead of fetching all rows) + db + .select({ status: resident.status, count: count() }) + .from(resident) + .groupBy(resident.status), + // Count of ACTIVE residents with no active placement (separate query) + db.$count( + resident, + and( + eq(resident.status, 'ACTIVE'), + notInArray( + resident.id, + db + .select({ residentId: placement.residentId }) + .from(placement) + .where(eq(placement.status, 'ACTIVE')), + ), + ), + ), + // "My clients" — IDs where this user is a care worker + currentUser ? getMyResidentIds(currentUser.id) : Promise.resolve([]), + // Recent interpersonal incidents per subject (was Prisma's filtered + // `_count.incidentsAsSubject` select — the query API has no filtered + // relation count, so it is one grouped query joined in application code) + db + .select({ subjectId: incident.subjectId, count: count() }) + .from(incident) + .where( + and( + isNotNull(incident.subjectId), + gte(incident.date, getDateDaysAgo(30)), + eq(incident.category, 'INTERPERSONAL'), + ), + ) + .groupBy(incident.subjectId), + ], + ) + + const incidentCountByResident = new Map(incidentGroups.map((g) => [g.subjectId, g.count])) const statusCounts = statusGroups.reduce>((acc, g) => { - acc[g.status] = g._count._all + acc[g.status] = g.count return acc }, {}) const stats = { - total: statusGroups.reduce((sum, g) => sum + g._count._all, 0), + total: statusGroups.reduce((sum, g) => sum + g.count, 0), active: statusCounts.ACTIVE ?? 0, placed: statusCounts.PLACED ?? 0, archived: statusCounts.EXITED ?? 0, @@ -202,7 +220,7 @@ export default async function ResidentsListPage({ searchParams }: Props) { careAttributes: (r.careAttributes ?? []).filter( (a: { domain: string }) => !viewerDomain || a.domain === viewerDomain, ), - incidentCount: r._count?.incidentsAsSubject ?? 0, + incidentCount: incidentCountByResident.get(r.id) ?? 0, daysSinceCheckIn, checkInIntervalDays: intervalDays, isMyClient: myResidentIdSet.has(r.id), @@ -347,7 +365,7 @@ export default async function ResidentsListPage({ searchParams }: Props) { ({ ...r, - incidentCount: r._count?.incidentsAsSubject ?? 0, + incidentCount: incidentCountByResident.get(r.id) ?? 0, }))} canWrite={ viewerRole === 'ADMIN' || viewerRole === 'BETREUUNG' || viewerRole === 'SOZIALARBEIT' diff --git a/src/app/(admin)/settings/page.tsx b/src/app/(admin)/settings/page.tsx index e8a1ca6f..6ab404e9 100644 --- a/src/app/(admin)/settings/page.tsx +++ b/src/app/(admin)/settings/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { requirePermission, hasPermission } from '@/lib/auth' -import { prisma } from '@/lib/db' +import { db, user as userTable } from '@/lib/db' +import { eq, asc } from 'drizzle-orm' import { InviteForm } from './InviteForm' import { EMAIL_CONFIG } from '@/lib/email/config' import { @@ -43,9 +44,9 @@ export default async function SettingsPage() { const canConfigure = true const [staffUsers, systemConfig] = await Promise.all([ - prisma.user.findMany({ - where: { active: true }, - select: { + db.query.user.findMany({ + where: eq(userTable.active, true), + columns: { id: true, // Deliberately NOT selecting `code`. Even an administrator has no // reason to READ a colleague's credential: they can invite, deactivate @@ -58,9 +59,11 @@ export default async function SettingsPage() { scope: true, isSystemAdmin: true, lastLoginAt: true, - account: { select: { email: true } }, }, - orderBy: { name: 'asc' }, + with: { + account: { columns: { email: true } }, + }, + orderBy: [asc(userTable.name)], }), getSystemConfig(), ]) diff --git a/src/app/api/auth/demo/route.ts b/src/app/api/auth/demo/route.ts index 4d8f98ad..d4d3912b 100644 --- a/src/app/api/auth/demo/route.ts +++ b/src/app/api/auth/demo/route.ts @@ -2,7 +2,8 @@ import { NextRequest, NextResponse } from 'next/server' import { loginByCode, setSessionCookie } from '@/lib/auth' import { checkRateLimit, recordLoginAttempt, getClientIp } from '@/lib/auth/rate-limit' import { logger } from '@/lib/logger' -import { prisma } from '@/lib/db' +import { db, user, resident } from '@/lib/db' +import { and, eq, inArray } from 'drizzle-orm' import { setResidentCookie } from '@/lib/portal-auth' import { isDemoEnabled, resolveDemoResidentCode } from '@/lib/demo/config' import { demoStaffDoors } from '@/lib/demo/roles' @@ -36,9 +37,9 @@ async function availableDoors(): Promise { if (!isDemoEnabled()) return [] const codes = demoStaffDoors().map((door) => door.code) - const present = await prisma.user.findMany({ - where: { code: { in: codes }, active: true }, - select: { code: true }, + const present = await db.query.user.findMany({ + where: and(inArray(user.code, codes), eq(user.active, true)), + columns: { code: true }, }) const live = new Set(present.map((user) => user.code)) @@ -51,11 +52,11 @@ async function availableDoors(): Promise { const residentCode = resolveDemoResidentCode() if (residentCode) { - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true }, + const residentRow = await db.query.resident.findFirst({ + where: eq(resident.code, residentCode), + columns: { id: true }, }) - if (resident) doors.push({ id: 'resident', label: residentDoorLabel() }) + if (residentRow) doors.push({ id: 'resident', label: residentDoorLabel() }) } return doors diff --git a/src/app/api/auth/invite/route.ts b/src/app/api/auth/invite/route.ts index df906c84..e7183e9e 100644 --- a/src/app/api/auth/invite/route.ts +++ b/src/app/api/auth/invite/route.ts @@ -1,4 +1,6 @@ -import { prisma } from '@/lib/db' +import { db, user as userTable, account, isUniqueViolation } from '@/lib/db' +import { eq } from 'drizzle-orm' +import { DatabaseError } from 'pg' import { NextRequest, NextResponse } from 'next/server' import { getCurrentUser } from '@/lib/auth' import { @@ -13,7 +15,20 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' import { generateStaffCode } from '@/lib/auth/code-generation' import { checkRateLimit, recordLoginAttempt, getClientIp } from '@/lib/auth/rate-limit' import { logger } from '@/lib/logger' -import { Prisma } from '@prisma/client' + +/** + * The violated constraint name from a pg unique-violation, walking wrapped + * causes (drizzle wraps the driver error). Prisma exposed the violated + * COLUMNS via `error.meta.target`; pg names the CONSTRAINT instead + * ('Account_email_key' / 'User_code_key'). + */ +function uniqueViolationConstraint(error: unknown): string | undefined { + if (error instanceof DatabaseError) return error.constraint + if (error instanceof Error && error.cause !== undefined) { + return uniqueViolationConstraint(error.cause) + } + return undefined +} /** * POST /api/auth/invite @@ -89,7 +104,7 @@ export async function POST(request: NextRequest) { let code: string | null = null for (let attempt = 0; attempt < 5; attempt++) { const candidate = generateStaffCode() - const existing = await prisma.user.findUnique({ where: { code: candidate } }) + const existing = await db.query.user.findFirst({ where: eq(userTable.code, candidate) }) if (!existing) { code = candidate break @@ -103,9 +118,9 @@ export async function POST(request: NextRequest) { } // Check if email already registered - const existingEmail = await prisma.account.findUnique({ - where: { email: email.toLowerCase() }, - select: { id: true }, + const existingEmail = await db.query.account.findFirst({ + where: eq(account.email, email.toLowerCase()), + columns: { id: true }, }) if (existingEmail) { return NextResponse.json( @@ -120,30 +135,35 @@ export async function POST(request: NextRequest) { // The invited person's email is KNOWN but they have no password yet — // exactly the shape an unclaimed Account has, so /register (or // /forgot-password) completes it without an admin doing anything else. - user = await prisma.user.create({ - data: { - code, - name: name.trim(), - role, - // Stated, not inherited from the column defaults. An invited colleague - // gets their own domain and administers nothing until someone widens - // that deliberately — same answer the defaults give, but written down, - // so a future change to the defaults cannot quietly widen every invite. - scope: NARROWEST_CAPABILITIES.scope, - isSystemAdmin: NARROWEST_CAPABILITIES.isSystemAdmin, - active: true, - account: { create: { email: email.toLowerCase() } }, - }, - select: { id: true, code: true, name: true, account: { select: { email: true } } }, + user = await db.transaction(async (tx) => { + const [created] = await tx + .insert(userTable) + .values({ + code, + name: name.trim(), + role, + // Stated, not inherited from the column defaults. An invited colleague + // gets their own domain and administers nothing until someone widens + // that deliberately — same answer the defaults give, but written down, + // so a future change to the defaults cannot quietly widen every invite. + scope: NARROWEST_CAPABILITIES.scope, + isSystemAdmin: NARROWEST_CAPABILITIES.isSystemAdmin, + active: true, + }) + .returning({ id: userTable.id, code: userTable.code, name: userTable.name }) + const [createdAccount] = await tx + .insert(account) + .values({ email: email.toLowerCase(), userId: created.id }) + .returning({ email: account.email }) + return { ...created, account: { email: createdAccount.email } } }) } catch (error) { // Race between pre-check and insert: surface conflict explicitly. - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { - const target = (error.meta as { target?: string[] } | undefined)?.target?.[0] - const message = - target === 'email' - ? 'Diese E-Mail-Adresse ist bereits registriert' - : 'Dieser Code ist bereits vergeben' + if (isUniqueViolation(error)) { + const constraint = uniqueViolationConstraint(error) + const message = constraint?.includes('email') + ? 'Diese E-Mail-Adresse ist bereits registriert' + : 'Dieser Code ist bereits vergeben' return NextResponse.json({ success: false, error: message }, { status: 409 }) } logger.errorWithCause('Failed to invite staff user', error, { email }) diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index c5546f11..5383e2b7 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,5 +1,6 @@ import { BRAND } from '@/lib/config/brand' -import { prisma } from '@/lib/db' +import { db, user as userTable, isUniqueViolation } from '@/lib/db' +import { eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getCurrentUser } from '@/lib/auth' import { @@ -13,7 +14,6 @@ import { import { ERROR_MESSAGES } from '@/lib/constants/error-messages' import { generateStaffCode } from '@/lib/auth/code-generation' import { logger } from '@/lib/logger' -import { Prisma } from '@prisma/client' /** * Staff user provisioning (Leitung only). @@ -114,7 +114,7 @@ export async function POST(request: NextRequest) { // Generate a unique code for (let attempt = 0; attempt < 5; attempt++) { const candidate = generateStaffCode() - const existing = await prisma.user.findUnique({ where: { code: candidate } }) + const existing = await db.query.user.findFirst({ where: eq(userTable.code, candidate) }) if (!existing) { code = candidate break @@ -129,7 +129,7 @@ export async function POST(request: NextRequest) { } // Check code uniqueness - const existing = await prisma.user.findUnique({ where: { code } }) + const existing = await db.query.user.findFirst({ where: eq(userTable.code, code) }) if (existing) { return NextResponse.json( { success: false, error: 'Dieser Code ist bereits vergeben' }, @@ -138,17 +138,24 @@ export async function POST(request: NextRequest) { } try { - const user = await prisma.user.create({ - data: { + const [user] = await db + .insert(userTable) + .values({ code, name: name.trim(), role, scope, isSystemAdmin, active: true, - }, - select: { id: true, code: true, name: true, role: true, scope: true, isSystemAdmin: true }, - }) + }) + .returning({ + id: userTable.id, + code: userTable.code, + name: userTable.name, + role: userTable.role, + scope: userTable.scope, + isSystemAdmin: userTable.isSystemAdmin, + }) return NextResponse.json({ success: true, @@ -156,7 +163,7 @@ export async function POST(request: NextRequest) { }) } catch (error) { // Race between unique pre-check and create: report friendly conflict. - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { return NextResponse.json( { success: false, error: 'Dieser Code ist bereits vergeben' }, { status: 409 }, diff --git a/src/app/api/chores/route.ts b/src/app/api/chores/route.ts index 8b70899c..0b07299f 100644 --- a/src/app/api/chores/route.ts +++ b/src/app/api/chores/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { prisma } from '@/lib/db' +import { db, housingUnit, householdTask } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requireStaffAuth } from '@/lib/auth' import { portalCreateTaskSchema, ValidationError, validateFormData } from '@/lib/validation/schemas' import { logAudit } from '@/lib/audit' @@ -72,9 +73,9 @@ export async function POST(request: NextRequest) { // Checked rather than trusted: an id from a form is an id a caller chose, // and creating a task against a unit that does not exist would fail with a // foreign-key error rather than a message anybody can act on. - const unit = await prisma.housingUnit.findUnique({ - where: { id: unitId }, - select: { id: true, code: true }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, unitId), + columns: { id: true, code: true }, }) if (!unit) { return NextResponse.json( @@ -83,8 +84,9 @@ export async function POST(request: NextRequest) { ) } - const task = await prisma.householdTask.create({ - data: { + const [task] = await db + .insert(householdTask) + .values({ housingUnitId: unit.id, createdByStaff: staff.name, title: data.title, @@ -96,8 +98,8 @@ export async function POST(request: NextRequest) { scheduleHuman: data.scheduleHuman || null, estimatedMinutes: data.estimatedMinutes || null, checklist: data.checklist ?? [], - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', diff --git a/src/app/api/cron/notifications/route.ts b/src/app/api/cron/notifications/route.ts index 40fe66fc..775fb544 100644 --- a/src/app/api/cron/notifications/route.ts +++ b/src/app/api/cron/notifications/route.ts @@ -12,7 +12,8 @@ import { BRAND } from '@/lib/config/brand' import { NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, incident, placement, satisfactionCheckIn } from '@/lib/db' +import { and, desc, eq, inArray, isNull, lt, sql } from 'drizzle-orm' import { notifyStaff, incidentFollowUpReminder, checkInReminder } from '@/lib/email' import { logger } from '@/lib/logger' import { DISPLAY_LIMITS } from '@/lib/config/thresholds' @@ -44,9 +45,10 @@ export async function GET(request: Request) { // Postgres advisory lock — non-blocking, session-scoped. If another // invocation of this same cron is already running on the same DB, this // call returns false and we bail. Prevents overlapping-run double-sends. - const lockResult = await prisma.$queryRaw>` + const { rows: lockRows } = await db.execute(sql` SELECT pg_try_advisory_lock(${CRON_LOCK_KEY}) AS ok - ` + `) + const lockResult = lockRows as unknown as Array<{ ok: boolean }> if (!lockResult[0]?.ok) { logger.warn('cron/notifications skipped: another run already in progress') return NextResponse.json({ skipped: true, reason: 'lock-held' }) @@ -65,16 +67,16 @@ export async function GET(request: Request) { try { // 1. Overdue incident follow-ups - const overdueIncidents = await prisma.incident.findMany({ - where: { - resolvedAt: null, - nextFollowUpDate: { lt: new Date() }, - severity: { in: ['MEDIUM', 'HIGH', 'CRITICAL'] }, + const overdueIncidents = await db.query.incident.findMany({ + where: and( + isNull(incident.resolvedAt), + lt(incident.nextFollowUpDate, new Date()), + inArray(incident.severity, ['MEDIUM', 'HIGH', 'CRITICAL']), + ), + with: { + housingUnit: { columns: { code: true } }, }, - include: { - housingUnit: { select: { code: true } }, - }, - take: 50, + limit: 50, }) if (overdueIncidents.length > 0) { @@ -94,13 +96,13 @@ export async function GET(request: Request) { // 2. Overdue resident check-ins. // Cap to a sane batch so a runaway dataset can't blow the function // timeout. The cron runs daily so we'd catch any backlog the next day. - const activePlacements = await prisma.placement.findMany({ - where: { status: 'ACTIVE' }, - include: { - resident: { select: { code: true, supportLevel: true } }, - checkIns: { orderBy: { createdAt: 'desc' }, take: 1 }, + const activePlacements = await db.query.placement.findMany({ + where: eq(placement.status, 'ACTIVE'), + with: { + resident: { columns: { code: true, supportLevel: true } }, + checkIns: { orderBy: [desc(satisfactionCheckIn.createdAt)], limit: 1 }, }, - take: 1000, + limit: 1000, }) const overdueResidents = activePlacements @@ -128,7 +130,7 @@ export async function GET(request: Request) { // rule book), and relying on someone remembering to press a button in the // admin UI is how a database ends up silently missing it. Idempotent, so // this is a no-op on every run that changes nothing. - const ruleSync = await syncOrgRules(prisma) + const ruleSync = await syncOrgRules(db) results.orgRulesCreated = ruleSync.created results.orgRulesAmended = ruleSync.amended if (ruleSync.created > 0 || ruleSync.amended > 0) { @@ -157,7 +159,7 @@ export async function GET(request: Request) { // Release the advisory lock. Failure here is logged but never thrown, // so it can't mask the original error. try { - await prisma.$queryRaw`SELECT pg_advisory_unlock(${CRON_LOCK_KEY})` + await db.execute(sql`SELECT pg_advisory_unlock(${CRON_LOCK_KEY})`) } catch (unlockErr) { logger.errorWithCause('Failed to release cron advisory lock', unlockErr) } diff --git a/src/app/api/cron/reset-demo/route.ts b/src/app/api/cron/reset-demo/route.ts index 8b29392e..37bccdf0 100644 --- a/src/app/api/cron/reset-demo/route.ts +++ b/src/app/api/cron/reset-demo/route.ts @@ -10,7 +10,8 @@ * deployments only. Explicit opt-in. */ import { NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { sql } from 'drizzle-orm' +import { db } from '@/lib/db' import { logger } from '@/lib/logger' import { isDemoEnabled, getDemoResetScope } from '@/lib/demo/config' import { resetDemoData } from '@/lib/demo/reset' @@ -41,9 +42,10 @@ export async function POST(request: Request) { // Advisory lock: a reset overlapping itself (or a slow previous run) would // truncate mid-seed. Non-blocking — the later run just skips. - const lockResult = await prisma.$queryRaw>` + const { rows: lockRows } = await db.execute(sql` SELECT pg_try_advisory_lock(${CRON_LOCK_KEY}) AS ok - ` + `) + const lockResult = lockRows as unknown as Array<{ ok: boolean }> if (!lockResult[0]?.ok) { logger.warn('cron/reset-demo skipped: another run already in progress') return NextResponse.json({ skipped: true, reason: 'lock-held' }) @@ -51,7 +53,7 @@ export async function POST(request: Request) { try { const scope = getDemoResetScope() - const summary = scope === 'FULL' ? await resetDemoData(prisma) : await resetDemoWorld(prisma) + const summary = scope === 'FULL' ? await resetDemoData(db) : await resetDemoWorld(db) logger.info('Demo data reset', { scope, ...summary }) return NextResponse.json({ success: true, scope, ...summary }) } catch (error) { @@ -61,7 +63,7 @@ export async function POST(request: Request) { // Release the advisory lock. Failure here is logged but never thrown, // so it cannot mask the reset's own outcome. try { - await prisma.$queryRaw`SELECT pg_advisory_unlock(${CRON_LOCK_KEY})` + await db.execute(sql`SELECT pg_advisory_unlock(${CRON_LOCK_KEY})`) } catch (unlockErr) { logger.errorWithCause('Failed to release cron advisory lock', unlockErr) } diff --git a/src/app/api/export/incidents/route.ts b/src/app/api/export/incidents/route.ts index bd9b7475..766d311b 100644 --- a/src/app/api/export/incidents/route.ts +++ b/src/app/api/export/incidents/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' import { authorizeStaff } from '@/lib/auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' -import { prisma } from '@/lib/db' +import { db, incident } from '@/lib/db' +import { desc } from 'drizzle-orm' import { generateCSV } from '@/lib/export' import { EXPORT_COLUMNS } from '@/lib/export/config' @@ -18,8 +19,8 @@ export async function GET() { } try { - const data = await prisma.incident.findMany({ - orderBy: { date: 'desc' }, + const data = await db.query.incident.findMany({ + orderBy: [desc(incident.date)], }) const csv = generateCSV(data as unknown as Record[], EXPORT_COLUMNS.incidents) diff --git a/src/app/api/export/placements/route.ts b/src/app/api/export/placements/route.ts index 9b6388fd..18ffa3cf 100644 --- a/src/app/api/export/placements/route.ts +++ b/src/app/api/export/placements/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' import { authorizeStaff } from '@/lib/auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' -import { prisma } from '@/lib/db' +import { db, placement } from '@/lib/db' +import { desc } from 'drizzle-orm' import { generateCSV } from '@/lib/export' import { EXPORT_COLUMNS } from '@/lib/export/config' @@ -18,8 +19,8 @@ export async function GET() { } try { - const data = await prisma.placement.findMany({ - orderBy: { startDate: 'desc' }, + const data = await db.query.placement.findMany({ + orderBy: [desc(placement.startDate)], }) const csv = generateCSV(data as unknown as Record[], EXPORT_COLUMNS.placements) diff --git a/src/app/api/export/residents/route.ts b/src/app/api/export/residents/route.ts index f10bb66e..29f9d0d1 100644 --- a/src/app/api/export/residents/route.ts +++ b/src/app/api/export/residents/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' import { authorizeStaff } from '@/lib/auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' -import { prisma } from '@/lib/db' +import { db, resident } from '@/lib/db' +import { desc } from 'drizzle-orm' import { generateCSV } from '@/lib/export' import { EXPORT_COLUMNS } from '@/lib/export/config' @@ -18,8 +19,8 @@ export async function GET() { } try { - const data = await prisma.resident.findMany({ - orderBy: { createdAt: 'desc' }, + const data = await db.query.resident.findMany({ + orderBy: [desc(resident.createdAt)], }) const csv = generateCSV(data as unknown as Record[], EXPORT_COLUMNS.residents) diff --git a/src/app/api/export/satisfaction/route.ts b/src/app/api/export/satisfaction/route.ts index 7e5da2b8..f669c14d 100644 --- a/src/app/api/export/satisfaction/route.ts +++ b/src/app/api/export/satisfaction/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' import { authorizeStaff } from '@/lib/auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' -import { prisma } from '@/lib/db' +import { db, satisfactionCheckIn } from '@/lib/db' +import { desc } from 'drizzle-orm' import { generateCSV } from '@/lib/export' import { EXPORT_COLUMNS } from '@/lib/export/config' @@ -18,8 +19,8 @@ export async function GET() { } try { - const data = await prisma.satisfactionCheckIn.findMany({ - orderBy: { createdAt: 'desc' }, + const data = await db.query.satisfactionCheckIn.findMany({ + orderBy: [desc(satisfactionCheckIn.createdAt)], }) const csv = generateCSV( diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts index 53487a11..9d980f13 100644 --- a/src/app/api/health/route.ts +++ b/src/app/api/health/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { sql } from 'drizzle-orm' +import { db } from '@/lib/db' import { getAIHealth } from '@/lib/ai/health' /** @@ -19,7 +20,7 @@ export async function GET() { const started = Date.now() const ai = getAIHealth() try { - await prisma.$queryRaw`SELECT 1` + await db.execute(sql`SELECT 1`) return NextResponse.json({ status: 'ok', db: 'up', diff --git a/src/app/api/import/residents/route.ts b/src/app/api/import/residents/route.ts index 517b4c6f..112107db 100644 --- a/src/app/api/import/residents/route.ts +++ b/src/app/api/import/residents/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server' import { authorizeStaff } from '@/lib/auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' -import { prisma } from '@/lib/db' +import { db, resident, auditLog } from '@/lib/db' +import { inArray } from 'drizzle-orm' import { ResidentImportSchema } from '@/lib/validation/import' import Papa from 'papaparse' import { logAudit } from '@/lib/audit' @@ -80,17 +81,21 @@ export async function POST(request: Request) { return NextResponse.json({ success: true, ...results }) } - // Phase 2: one createMany with skipDuplicates so a single P2002 conflict - // doesn't abort the whole batch. Then a single audit insert per created - // resident is collected in one transaction so a partial failure is undone. + // Phase 2: one bulk insert with conflict-skip so a single unique-violation + // conflict doesn't abort the whole batch. Then a single audit insert per + // created resident is collected in one transaction so a partial failure is + // undone. try { - const createdRows = await prisma.$transaction(async (tx) => { + const createdRows = await db.transaction(async (tx) => { // Detect which codes already exist BEFORE the bulk insert, so we can - // report per-row "already exists" errors. `createMany` with - // `skipDuplicates` silently drops them — we want to surface them. - const existing = await tx.resident.findMany({ - where: { code: { in: validRows.map((v) => v.data.code) } }, - select: { code: true }, + // report per-row "already exists" errors. `onConflictDoNothing` + // silently drops them — we want to surface them. + const existing = await tx.query.resident.findMany({ + where: inArray( + resident.code, + validRows.map((v) => v.data.code), + ), + columns: { code: true }, }) const existingCodes = new Set(existing.map((r) => r.code)) @@ -107,33 +112,42 @@ export async function POST(request: Request) { if (toInsert.length === 0) return [] - await tx.resident.createMany({ - data: toInsert.map((v) => ({ - ...v.data, - privacyNeed: 3, - guestTolerance: 3, - status: 'ACTIVE', - })), + await tx + .insert(resident) + .values( + toInsert.map((v) => ({ + ...v.data, + privacyNeed: 3, + guestTolerance: 3, + status: 'ACTIVE' as const, + })), + ) // Race-safety: if a concurrent import inserts one of these codes - // between our pre-check and the createMany, skip rather than throw. - skipDuplicates: true, + // between our pre-check and the bulk insert, skip rather than throw. + .onConflictDoNothing() + + // We need the IDs for the audit log; the bulk insert doesn't return them. + const inserted = await tx.query.resident.findMany({ + where: inArray( + resident.code, + toInsert.map((v) => v.data.code), + ), + columns: { id: true, code: true }, }) - // We need the IDs for the audit log; createMany doesn't return them. - const inserted = await tx.resident.findMany({ - where: { code: { in: toInsert.map((v) => v.data.code) } }, - select: { id: true, code: true }, - }) - - await tx.auditLog.createMany({ - data: inserted.map((r) => ({ - action: 'CREATE', - entity: 'RESIDENT', - entityId: r.id, - userId: user.id, - changes: { code: r.code, source: 'CSV_IMPORT' }, - })), - }) + // Guard: if the race above skipped every row, there is nothing to log + // (an empty `.values([])` throws, where Prisma's createMany no-oped). + if (inserted.length > 0) { + await tx.insert(auditLog).values( + inserted.map((r) => ({ + action: 'CREATE', + entity: 'RESIDENT', + entityId: r.id, + userId: user.id, + changes: { code: r.code, source: 'CSV_IMPORT' }, + })), + ) + } return inserted }) diff --git a/src/app/api/portal/apartment/route.ts b/src/app/api/portal/apartment/route.ts index 0062e645..bb3c1166 100644 --- a/src/app/api/portal/apartment/route.ts +++ b/src/app/api/portal/apartment/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, housingUnit } from '@/lib/db' +import { eq } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { UpdateApartmentSchema } from '@/lib/validation/expenses' import { logAudit } from '@/lib/audit' @@ -26,11 +27,11 @@ export async function PATCH(request: NextRequest) { try { // Any current resident may (re)name their own home; the audit trail // records who did. An empty string clears the nickname. - const unit = await prisma.housingUnit.update({ - where: { id: auth.placement.housingUnitId }, - data: { nickname: parsed.data.nickname || null }, - select: { id: true, nickname: true }, - }) + const [unit] = await db + .update(housingUnit) + .set({ nickname: parsed.data.nickname || null }) + .where(eq(housingUnit.id, auth.placement.housingUnitId)) + .returning({ id: housingUnit.id, nickname: housingUnit.nickname }) await logAudit({ action: 'UPDATE', diff --git a/src/app/api/portal/chores/[id]/attention/route.ts b/src/app/api/portal/chores/[id]/attention/route.ts index 34f86427..c2daf6d9 100644 --- a/src/app/api/portal/chores/[id]/attention/route.ts +++ b/src/app/api/portal/chores/[id]/attention/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, taskAttentionFlag } from '@/lib/db' +import { eq, and } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { portalAttentionFlagSchema } from '@/lib/validation/schemas' @@ -31,8 +32,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } try { - const task = await prisma.householdTask.findFirst({ - where: { id, housingUnitId: auth.placement.housingUnitId }, + const task = await db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, auth.placement.housingUnitId), + ), }) if (!task) { @@ -49,19 +53,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ ) } - const flag = await prisma.taskAttentionFlag.create({ - data: { + const [flag] = await db + .insert(taskAttentionFlag) + .values({ taskId: id, flaggedById: auth.resident.id, message: message || null, - }, - }) + }) + .returning() // Update task status to NEEDS_ATTENTION - await prisma.householdTask.update({ - where: { id }, - data: { currentStatus: 'NEEDS_ATTENTION' }, - }) + await db + .update(householdTask) + .set({ currentStatus: 'NEEDS_ATTENTION' }) + .where(eq(householdTask.id, id)) return NextResponse.json({ success: true, data: flag }) } catch (error) { diff --git a/src/app/api/portal/chores/[id]/complaint/route.ts b/src/app/api/portal/chores/[id]/complaint/route.ts index 10ce76a1..e71fd2f3 100644 --- a/src/app/api/portal/chores/[id]/complaint/route.ts +++ b/src/app/api/portal/chores/[id]/complaint/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, incident } from '@/lib/db' +import { eq, and } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { portalTaskComplaintSchema } from '@/lib/validation/schemas' @@ -41,8 +42,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } try { - const task = await prisma.householdTask.findFirst({ - where: { id, housingUnitId: auth.placement.housingUnitId }, + const task = await db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, auth.placement.housingUnitId), + ), }) if (!task) { @@ -52,13 +56,14 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ ) } - // Map chore category to incident type. The map is typed against the Prisma + // Map chore category to incident type. The map is typed against the db // enums so the fallback is only used for unknown categories. const incidentType = CHORE_COMPLAINT_INCIDENT_MAP[task.category] ?? 'PERSONAL_CONFLICT' // Create an Incident (escalation to staff) - const incident = await prisma.incident.create({ - data: { + const [createdIncident] = await db + .insert(incident) + .values({ housingUnitId: auth.placement.housingUnitId, placementId: auth.placement.id, reportedById: auth.resident.id, @@ -67,13 +72,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ severity: 'MEDIUM', description: `[Haushaltsaufgabe: ${task.title}]\n\n${description}`, date: new Date(), - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', entity: 'INCIDENT', - entityId: incident.id, + entityId: createdIncident.id, changes: { source: 'household_task_complaint', taskId: id, @@ -82,7 +87,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ }, }) - return NextResponse.json({ success: true, data: { incidentId: incident.id } }) + return NextResponse.json({ success: true, data: { incidentId: createdIncident.id } }) } catch (error) { logger.errorWithCause('Failed to create task complaint incident', error) return NextResponse.json( diff --git a/src/app/api/portal/chores/[id]/complete/route.ts b/src/app/api/portal/chores/[id]/complete/route.ts index 5f037216..2995d15a 100644 --- a/src/app/api/portal/chores/[id]/complete/route.ts +++ b/src/app/api/portal/chores/[id]/complete/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, taskCompletion, taskAttentionFlag, taskRequest } from '@/lib/db' +import { eq, and, inArray } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { portalCompleteTaskSchema } from '@/lib/validation/schemas' @@ -37,8 +38,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } // Security check: verify task belongs to this housing unit - const task = await prisma.householdTask.findFirst({ - where: { id, housingUnitId: auth.placement.housingUnitId }, + const task = await db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, auth.placement.housingUnitId), + ), }) if (!task) { @@ -57,13 +61,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Only items that are actually on this task's checklist may be recorded — // otherwise a client could claim credit for work the house never agreed on. - const ticked = (completedItems ?? []).filter((item) => task.checklist.includes(item)) + const ticked = (completedItems ?? []).filter((item) => (task.checklist ?? []).includes(item)) try { // Transaction: create completion + update task + resolve flags + complete requests. // For ONE_TIME tasks we use a conditional update with `isCompleted: false` guard // to prevent two concurrent completions from racing past the outer check. - const result = await prisma.$transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const isOneTime = task.taskType === 'ONE_TIME' // 1. Create completion. Ticked items are intersected with the task's own @@ -74,46 +78,46 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ ? (task.checklist ?? []).filter((item) => completedItems!.includes(item)) : [] - const completion = await tx.taskCompletion.create({ - data: { + const [completion] = await tx + .insert(taskCompletion) + .values({ taskId: id, completedById: auth.resident.id, notes: notes || null, durationMinutes: durationMinutes || null, completedItems: tickedItems, - }, - }) + }) + .returning() // 2. Update task status - await tx.householdTask.update({ - where: { id }, - data: { + await tx + .update(householdTask) + .set({ currentStatus: 'IDLE', ...(isOneTime ? { isCompleted: true, completedAt: new Date() } : {}), - }, - }) + }) + .where(eq(householdTask.id, id)) // 3. Resolve active attention flags - await tx.taskAttentionFlag.updateMany({ - where: { taskId: id, isResolved: false }, - data: { + await tx + .update(taskAttentionFlag) + .set({ isResolved: true, resolvedAt: new Date(), resolvedByCompletionId: completion.id, - }, - }) + }) + .where(and(eq(taskAttentionFlag.taskId, id), eq(taskAttentionFlag.isResolved, false))) // 4. Complete pending/accepted requests - await tx.taskRequest.updateMany({ - where: { - taskId: id, - status: { in: ['PENDING', 'ACCEPTED'] }, - }, - data: { + await tx + .update(taskRequest) + .set({ status: 'COMPLETED', completionId: completion.id, - }, - }) + }) + .where( + and(eq(taskRequest.taskId, id), inArray(taskRequest.status, ['PENDING', 'ACCEPTED'])), + ) return completion }) diff --git a/src/app/api/portal/chores/[id]/request/route.ts b/src/app/api/portal/chores/[id]/request/route.ts index 2d149dae..2ef7b96e 100644 --- a/src/app/api/portal/chores/[id]/request/route.ts +++ b/src/app/api/portal/chores/[id]/request/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, taskRequest, placement } from '@/lib/db' +import { eq, and } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { portalTaskRequestSchema } from '@/lib/validation/schemas' @@ -37,8 +38,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } try { - const task = await prisma.householdTask.findFirst({ - where: { id, housingUnitId: auth.placement.housingUnitId }, + const task = await db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, auth.placement.housingUnitId), + ), }) if (!task) { @@ -58,13 +62,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ // Validate requestedResidentId is a roommate (same housing unit) — prevents // targeting arbitrary residents elsewhere in the system. if (requestedResidentId) { - const roommate = await prisma.placement.findFirst({ - where: { - residentId: requestedResidentId, - housingUnitId: auth.placement.housingUnitId, - status: 'ACTIVE', - }, - select: { residentId: true }, + const roommate = await db.query.placement.findFirst({ + where: and( + eq(placement.residentId, requestedResidentId), + eq(placement.housingUnitId, auth.placement.housingUnitId), + eq(placement.status, 'ACTIVE'), + ), + columns: { residentId: true }, }) if (!roommate) { return NextResponse.json( @@ -76,23 +80,24 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const isBroadcast = !requestedResidentId - const taskRequest = await prisma.taskRequest.create({ - data: { + const [createdRequest] = await db + .insert(taskRequest) + .values({ taskId: id, requestedById: auth.resident.id, requestedResidentId: requestedResidentId || null, isBroadcast, message: message || null, - }, - }) + }) + .returning() // Update task status to REQUESTED - await prisma.householdTask.update({ - where: { id }, - data: { currentStatus: 'REQUESTED' }, - }) + await db + .update(householdTask) + .set({ currentStatus: 'REQUESTED' }) + .where(eq(householdTask.id, id)) - return NextResponse.json({ success: true, data: taskRequest }) + return NextResponse.json({ success: true, data: createdRequest }) } catch (error) { logger.errorWithCause('Failed to create task request', error) return NextResponse.json( diff --git a/src/app/api/portal/chores/[id]/route.ts b/src/app/api/portal/chores/[id]/route.ts index b36426ae..1207bd37 100644 --- a/src/app/api/portal/chores/[id]/route.ts +++ b/src/app/api/portal/chores/[id]/route.ts @@ -1,4 +1,12 @@ -import { prisma } from '@/lib/db' +import { + db, + householdTask, + taskCompletion, + taskAttentionFlag, + taskRequest, + placement, +} from '@/lib/db' +import { eq, and, ne, desc } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { logger } from '@/lib/logger' @@ -17,27 +25,27 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ const { id } = await params try { - const task = await prisma.householdTask.findFirst({ - where: { - id, - housingUnitId: auth.placement.housingUnitId, - }, - include: { - createdByResident: { select: { id: true, code: true } }, + const task = await db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, auth.placement.housingUnitId), + ), + with: { + createdByResident: { columns: { id: true, code: true } }, completions: { - orderBy: { completedAt: 'desc' }, - take: QUERY_LIMITS.choreHistory, - include: { completedBy: { select: { id: true, code: true } } }, + orderBy: [desc(taskCompletion.completedAt)], + limit: QUERY_LIMITS.choreHistory, + with: { completedBy: { columns: { id: true, code: true } } }, }, attentionFlags: { - orderBy: { createdAt: 'desc' }, - include: { flaggedBy: { select: { id: true, code: true } } }, + orderBy: [desc(taskAttentionFlag.createdAt)], + with: { flaggedBy: { columns: { id: true, code: true } } }, }, requests: { - orderBy: { createdAt: 'desc' }, - include: { - requestedBy: { select: { id: true, code: true } }, - requestedResident: { select: { id: true, code: true } }, + orderBy: [desc(taskRequest.createdAt)], + with: { + requestedBy: { columns: { id: true, code: true } }, + requestedResident: { columns: { id: true, code: true } }, }, }, }, @@ -51,14 +59,15 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{ } // Get roommates for request form - const roommates = await prisma.placement.findMany({ - where: { - housingUnitId: auth.placement.housingUnitId, - status: 'ACTIVE', - residentId: { not: auth.resident.id }, - }, - select: { - resident: { select: { id: true, code: true } }, + const roommates = await db.query.placement.findMany({ + where: and( + eq(placement.housingUnitId, auth.placement.housingUnitId), + eq(placement.status, 'ACTIVE'), + ne(placement.residentId, auth.resident.id), + ), + columns: {}, + with: { + resident: { columns: { id: true, code: true } }, }, }) diff --git a/src/app/api/portal/chores/route.ts b/src/app/api/portal/chores/route.ts index a769f233..2abd056c 100644 --- a/src/app/api/portal/chores/route.ts +++ b/src/app/api/portal/chores/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, taskCompletion, taskAttentionFlag, taskRequest } from '@/lib/db' +import { eq, desc, inArray } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { getPortalAuth } from '@/lib/portal-auth' import { portalCreateTaskSchema, ValidationError, validateFormData } from '@/lib/validation/schemas' @@ -17,23 +18,27 @@ export async function GET() { } try { - const tasks = await prisma.householdTask.findMany({ - where: { housingUnitId: auth.placement.housingUnitId }, - include: { + const tasks = await db.query.householdTask.findMany({ + where: eq(householdTask.housingUnitId, auth.placement.housingUnitId), + with: { completions: { - orderBy: { completedAt: 'desc' }, - take: 1, - include: { completedBy: { select: { id: true, code: true } } }, + orderBy: [desc(taskCompletion.completedAt)], + limit: 1, + with: { completedBy: { columns: { id: true, code: true } } }, }, attentionFlags: { - where: { isResolved: false }, + where: eq(taskAttentionFlag.isResolved, false), }, requests: { - where: { status: { in: ['PENDING', 'ACCEPTED'] } }, + where: inArray(taskRequest.status, ['PENDING', 'ACCEPTED']), }, - createdByResident: { select: { id: true, code: true } }, + createdByResident: { columns: { id: true, code: true } }, }, - orderBy: [{ currentStatus: 'desc' }, { priority: 'desc' }, { createdAt: 'desc' }], + orderBy: [ + desc(householdTask.currentStatus), + desc(householdTask.priority), + desc(householdTask.createdAt), + ], }) // Same loader the page uses — a balance that disagreed between the two @@ -74,8 +79,9 @@ export async function POST(request: NextRequest) { } try { - const task = await prisma.householdTask.create({ - data: { + const [task] = await db + .insert(householdTask) + .values({ housingUnitId: auth.placement.housingUnitId, createdByResidentId: auth.resident.id, title: data.title, @@ -87,8 +93,8 @@ export async function POST(request: NextRequest) { scheduleHuman: data.scheduleHuman || null, estimatedMinutes: data.estimatedMinutes || null, checklist: data.checklist ?? [], - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', diff --git a/src/app/api/portal/complaints/route.ts b/src/app/api/portal/complaints/route.ts index 33113744..1d280d65 100644 --- a/src/app/api/portal/complaints/route.ts +++ b/src/app/api/portal/complaints/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, resident as residentTable, complaint } from '@/lib/db' +import { eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { logger } from '@/lib/logger' @@ -41,9 +42,9 @@ export async function POST(request: NextRequest) { // anyone on the internet, and a channel full of noise protects nobody. What // "anonymous" changes is whether the RECORD carries the identity, not // whether the sender had one. - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + columns: { id: true }, }) if (!resident) { return NextResponse.json( @@ -63,12 +64,10 @@ export async function POST(request: NextRequest) { } try { - await prisma.complaint.create({ - data: { - residentId: parsed.anonymous ? null : resident.id, - subject: parsed.subject, - body: parsed.body, - }, + await db.insert(complaint).values({ + residentId: parsed.anonymous ? null : resident.id, + subject: parsed.subject, + body: parsed.body, }) } catch (error) { // The body is a person's complaint. It never goes to the logger. diff --git a/src/app/api/portal/expenses/[id]/route.ts b/src/app/api/portal/expenses/[id]/route.ts index 48078c45..6eb29608 100644 --- a/src/app/api/portal/expenses/[id]/route.ts +++ b/src/app/api/portal/expenses/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, expense as expenseTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { logAudit } from '@/lib/audit' import { logger } from '@/lib/logger' @@ -16,9 +17,9 @@ export async function DELETE(_request: NextRequest, props: { params: Promise<{ i } try { - const expense = await prisma.expense.findUnique({ - where: { id: params.id }, - select: { + const expense = await db.query.expense.findFirst({ + where: eq(expenseTable.id, params.id), + columns: { id: true, housingUnitId: true, paidById: true, @@ -43,7 +44,7 @@ export async function DELETE(_request: NextRequest, props: { params: Promise<{ i ) } - await prisma.expense.delete({ where: { id: expense.id } }) + await db.delete(expenseTable).where(eq(expenseTable.id, expense.id)) await logAudit({ action: 'DELETE', diff --git a/src/app/api/portal/expenses/route.ts b/src/app/api/portal/expenses/route.ts index ebe1a4ef..621f8cdd 100644 --- a/src/app/api/portal/expenses/route.ts +++ b/src/app/api/portal/expenses/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, expense as expenseTable, expenseShare } from '@/lib/db' import { getPortalAuth, getActiveUnitMembers } from '@/lib/portal-auth' import { CreateExpenseSchema } from '@/lib/validation/expenses' import { splitEqually } from '@/lib/expenses' @@ -43,18 +43,26 @@ export async function POST(request: NextRequest) { const shares = splitEqually(data.amountRappen, participantIds) - const expense = await prisma.expense.create({ - data: { - housingUnitId: auth.placement.housingUnitId, - paidById, - createdById: auth.resident.id, - description: data.description, - category: data.category, - amountRappen: data.amountRappen, - date: data.date ?? new Date(), - shares: { create: shares }, - }, - include: { shares: true }, + // Expense and its shares stand or fall together (Prisma's nested create was + // one transaction); the returned shape mirrors the old `include: { shares }`. + const expense = await db.transaction(async (tx) => { + const [created] = await tx + .insert(expenseTable) + .values({ + housingUnitId: auth.placement.housingUnitId, + paidById, + createdById: auth.resident.id, + description: data.description, + category: data.category, + amountRappen: data.amountRappen, + date: data.date ?? new Date(), + }) + .returning() + const shareRows = await tx + .insert(expenseShare) + .values(shares.map((share) => ({ ...share, expenseId: created.id }))) + .returning() + return { ...created, shares: shareRows } }) await logAudit({ diff --git a/src/app/api/portal/preferences/route.ts b/src/app/api/portal/preferences/route.ts index 8a8d3390..65a00231 100644 --- a/src/app/api/portal/preferences/route.ts +++ b/src/app/api/portal/preferences/route.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { logAudit } from '@/lib/audit' import { @@ -20,8 +21,8 @@ export async function POST(request: NextRequest) { ) } - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), }) if (!resident) { @@ -56,9 +57,9 @@ export async function POST(request: NextRequest) { .join('. ') || null try { - await prisma.resident.update({ - where: { id: resident.id }, - data: { + await db + .update(residentTable) + .set({ sleepSchedule: data.sleepSchedule, noiseTolerance: data.noiseTolerance, cleanlinessPractice: data.cleanlinessPractice, @@ -74,8 +75,8 @@ export async function POST(request: NextRequest) { dietaryNeeds: data.dietaryNeeds, roommatePreferences: roommatePrefsText, preferencesCompletedAt: new Date(), - }, - }) + }) + .where(eq(residentTable.id, resident.id)) await logAudit({ action: 'UPDATE', diff --git a/src/app/api/portal/profile/photo/route.ts b/src/app/api/portal/profile/photo/route.ts index 01b4deb9..ed56d4c4 100644 --- a/src/app/api/portal/profile/photo/route.ts +++ b/src/app/api/portal/profile/photo/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, residentPhoto } from '@/lib/db' +import { eq } from 'drizzle-orm' import { getPortalResident } from '@/lib/portal-auth' import { PHOTO_LIMITS, isAllowedPhotoMimeType } from '@/lib/config/profile' import { logAudit } from '@/lib/audit' @@ -40,11 +41,13 @@ export async function POST(request: NextRequest) { const data = Buffer.from(await file.arrayBuffer()) - await prisma.residentPhoto.upsert({ - where: { residentId: resident.id }, - update: { data, mimeType: file.type }, - create: { residentId: resident.id, data, mimeType: file.type }, - }) + await db + .insert(residentPhoto) + .values({ residentId: resident.id, data, mimeType: file.type }) + .onConflictDoUpdate({ + target: residentPhoto.residentId, + set: { data, mimeType: file.type }, + }) await logAudit({ action: 'UPDATE', @@ -73,7 +76,7 @@ export async function DELETE() { } try { - await prisma.residentPhoto.deleteMany({ where: { residentId: resident.id } }) + await db.delete(residentPhoto).where(eq(residentPhoto.residentId, resident.id)) await logAudit({ action: 'UPDATE', diff --git a/src/app/api/portal/profile/route.ts b/src/app/api/portal/profile/route.ts index c40b94fa..666549f5 100644 --- a/src/app/api/portal/profile/route.ts +++ b/src/app/api/portal/profile/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { getPortalResident } from '@/lib/portal-auth' import { UpdateProfileSchema } from '@/lib/validation/expenses' import { logAudit } from '@/lib/audit' @@ -27,9 +28,9 @@ export async function PATCH(request: NextRequest) { const data = parsed.data try { - const updated = await prisma.resident.update({ - where: { id: resident.id }, - data: { + const [updated] = await db + .update(residentTable) + .set({ // Empty string clears the field back to "code only". ...(data.displayName !== undefined && { displayName: data.displayName || null }), ...(data.bio !== undefined && { bio: data.bio || null }), @@ -39,15 +40,20 @@ export async function PATCH(request: NextRequest) { ...(data.profileVisibility !== undefined && { profileVisibility: data.profileVisibility, }), - }, - select: { - id: true, - code: true, - displayName: true, - bio: true, - profileVisibility: true, - }, - }) + // Set explicitly (rather than left to the schema's $onUpdateFn) because + // an all-optional PATCH body may leave every field above undefined, and + // drizzle throws on an empty `set` — Prisma treated that as a bare + // "touch" that still bumped updatedAt and returned the row. + updatedAt: new Date(), + }) + .where(eq(residentTable.id, resident.id)) + .returning({ + id: residentTable.id, + code: residentTable.code, + displayName: residentTable.displayName, + bio: residentTable.bio, + profileVisibility: residentTable.profileVisibility, + }) await logAudit({ action: 'UPDATE', diff --git a/src/app/api/portal/proposals/route.ts b/src/app/api/portal/proposals/route.ts index fe6d0292..04065288 100644 --- a/src/app/api/portal/proposals/route.ts +++ b/src/app/api/portal/proposals/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, houseRule, proposal as proposalTable } from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { logger } from '@/lib/logger' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -84,8 +85,8 @@ export async function POST(request: NextRequest) { let parentOrgRuleId: string | null = null if (parsed.data.type === 'ADD_RULE' && parsed.data.parentOrgRuleId) { - const parent = await prisma.houseRule.findUnique({ - where: { id: parsed.data.parentOrgRuleId }, + const parent = await db.query.houseRule.findFirst({ + where: eq(houseRule.id, parsed.data.parentOrgRuleId), }) if (!parent) { return NextResponse.json( @@ -104,9 +105,13 @@ export async function POST(request: NextRequest) { // A house rule that belongs to the house being changed, not another one. if (parsed.data.targetRuleId) { - const target = await prisma.houseRule.findFirst({ - where: { id: parsed.data.targetRuleId, scope: 'UNIT', housingUnitId }, - select: { id: true }, + const target = await db.query.houseRule.findFirst({ + where: and( + eq(houseRule.id, parsed.data.targetRuleId), + eq(houseRule.scope, 'UNIT'), + eq(houseRule.housingUnitId, housingUnitId), + ), + columns: { id: true }, }) if (!target) { return NextResponse.json( @@ -126,8 +131,9 @@ export async function POST(request: NextRequest) { // must answer it. "Not votable" must not mean "not heard". const goesStraightToStaff = decisionMode === 'STAFF_ONLY' || !canHoldVote(eligibleVoterCount) - const proposal = await prisma.proposal.create({ - data: { + const [proposal] = await db + .insert(proposalTable) + .values({ housingUnitId, type: parsed.data.type, category: parsed.data.category, @@ -146,9 +152,8 @@ export async function POST(request: NextRequest) { ? ERROR_MESSAGES.PROPOSAL_STAFF_ONLY : ERROR_MESSAGES.UNIT_TOO_SMALL_FOR_VOTE : null, - }, - select: { id: true, status: true }, - }) + }) + .returning({ id: proposalTable.id, status: proposalTable.status }) return NextResponse.json({ success: true, data: proposal }) } catch (error) { diff --git a/src/app/api/portal/proposals/vote/route.ts b/src/app/api/portal/proposals/vote/route.ts index 0e65c352..81d35599 100644 --- a/src/app/api/portal/proposals/vote/route.ts +++ b/src/app/api/portal/proposals/vote/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, proposal as proposalTable, vote } from '@/lib/db' +import { eq } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { logger } from '@/lib/logger' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -34,9 +35,9 @@ export async function POST(request: NextRequest) { // Close anything overdue first, so a vote cannot land after the deadline. await advanceDueProposals(new Date(), auth.placement.housingUnitId) - const proposal = await prisma.proposal.findUnique({ - where: { id: parsed.data.proposalId }, - select: { id: true, status: true, housingUnitId: true, votingEndsAt: true }, + const proposal = await db.query.proposal.findFirst({ + where: eq(proposalTable.id, parsed.data.proposalId), + columns: { id: true, status: true, housingUnitId: true, votingEndsAt: true }, }) if (!proposal) { @@ -68,22 +69,22 @@ export async function POST(request: NextRequest) { ) } - await prisma.vote.upsert({ - where: { - proposalId_residentId: { proposalId: proposal.id, residentId: auth.resident.id }, - }, - create: { + await db + .insert(vote) + .values({ proposalId: proposal.id, residentId: auth.resident.id, choice: parsed.data.choice, reason: parsed.data.reason, - }, - update: { - choice: parsed.data.choice, - reason: parsed.data.reason, - castAt: new Date(), - }, - }) + }) + .onConflictDoUpdate({ + target: [vote.proposalId, vote.residentId], + set: { + choice: parsed.data.choice, + reason: parsed.data.reason, + castAt: new Date(), + }, + }) return NextResponse.json({ success: true }) } catch (error) { diff --git a/src/app/api/portal/report/route.ts b/src/app/api/portal/report/route.ts index 1518660c..341269a1 100644 --- a/src/app/api/portal/report/route.ts +++ b/src/app/api/portal/report/route.ts @@ -1,4 +1,11 @@ -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + maintenanceRequest, + incident as incidentTable, +} from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { logAudit } from '@/lib/audit' import { portalReportSchema, validateFormData, ValidationError } from '@/lib/validation/schemas' @@ -28,13 +35,13 @@ export async function POST(request: NextRequest) { } // Derive resident and placement from cookie — never trust client-submitted IDs - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, - include: { housingUnit: { select: { code: true } } }, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, + with: { housingUnit: { columns: { code: true } } }, }, }, }) @@ -92,13 +99,13 @@ export async function POST(request: NextRequest) { data.involvedResident !== 'external' && data.involvedResident !== 'anonymous' ) { - const candidate = await prisma.placement.findFirst({ - where: { - residentId: data.involvedResident, - housingUnitId: placement.housingUnitId, - status: 'ACTIVE', - }, - select: { residentId: true }, + const candidate = await db.query.placement.findFirst({ + where: and( + eq(placementTable.residentId, data.involvedResident), + eq(placementTable.housingUnitId, placement.housingUnitId), + eq(placementTable.status, 'ACTIVE'), + ), + columns: { residentId: true }, }) if (!candidate) { return NextResponse.json( @@ -113,8 +120,9 @@ export async function POST(request: NextRequest) { // A broken tap goes to the maintenance board, not onto the conflict ladder. // @see lib/reports/routing.ts for why the two must not share a table. if (data.category === 'MAINTENANCE' && isMaintenanceType(data.type)) { - const request = await prisma.maintenanceRequest.create({ - data: { + const [request] = await db + .insert(maintenanceRequest) + .values({ housingUnitId: placement.housingUnitId, reportedById: resident.id, category: maintenanceCategoryFor(data.type), @@ -122,8 +130,8 @@ export async function POST(request: NextRequest) { title: getLabel(INCIDENT_TYPE_LABELS, data.type), description: data.description, location: locationLabel, - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', @@ -153,8 +161,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true }) } - const incident = await prisma.incident.create({ - data: { + const [incident] = await db + .insert(incidentTable) + .values({ housingUnitId: placement.housingUnitId, reportedById: resident.id, subjectId: validatedSubjectId, @@ -163,8 +172,8 @@ export async function POST(request: NextRequest) { severity: data.severity, description: fullDescription, date: data.incidentDate ? new Date(data.incidentDate) : new Date(), - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', diff --git a/src/app/api/portal/residents/[id]/photo/route.ts b/src/app/api/portal/residents/[id]/photo/route.ts index 04eda7bc..422170ad 100644 --- a/src/app/api/portal/residents/[id]/photo/route.ts +++ b/src/app/api/portal/residents/[id]/photo/route.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement, residentPhoto } from '@/lib/db' +import { and, eq, inArray } from 'drizzle-orm' +import { alias } from 'drizzle-orm/pg-core' import { getPortalResident } from '@/lib/portal-auth' import { getCurrentUser } from '@/lib/auth' import { logger } from '@/lib/logger' @@ -31,9 +33,9 @@ export async function GET(_request: NextRequest, props: { params: Promise<{ id: } try { - const subject = await prisma.resident.findUnique({ - where: { id: params.id }, - select: { id: true, profileVisibility: true }, + const subject = await db.query.resident.findFirst({ + where: eq(residentTable.id, params.id), + columns: { id: true, profileVisibility: true }, }) if (!subject) { return NextResponse.json( @@ -48,19 +50,31 @@ export async function GET(_request: NextRequest, props: { params: Promise<{ id: } else { // Only asked when it can change the answer: a resident looking at their // own photo, or a PRIVATE subject, needs no unit lookup. + // "Shares a unit" = the viewer has an ACTIVE placement in a unit where + // the subject also has one (Prisma's relation filter, as a subquery). + const subjectPlacement = alias(placement, 'subjectPlacement') const sharesUnit = resident!.id === params.id ? false : Boolean( - await prisma.placement.findFirst({ - where: { - residentId: resident!.id, - status: 'ACTIVE', - housingUnit: { - placements: { some: { residentId: params.id, status: 'ACTIVE' } }, - }, - }, - select: { id: true }, + await db.query.placement.findFirst({ + where: and( + eq(placement.residentId, resident!.id), + eq(placement.status, 'ACTIVE'), + inArray( + placement.housingUnitId, + db + .select({ housingUnitId: subjectPlacement.housingUnitId }) + .from(subjectPlacement) + .where( + and( + eq(subjectPlacement.residentId, params.id), + eq(subjectPlacement.status, 'ACTIVE'), + ), + ), + ), + ), + columns: { id: true }, }), ) viewer = { kind: 'resident', residentId: resident!.id, sharesUnit } @@ -73,7 +87,9 @@ export async function GET(_request: NextRequest, props: { params: Promise<{ id: ) } - const photo = await prisma.residentPhoto.findUnique({ where: { residentId: params.id } }) + const photo = await db.query.residentPhoto.findFirst({ + where: eq(residentPhoto.residentId, params.id), + }) if (!photo) { return NextResponse.json( { success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND }, diff --git a/src/app/api/portal/rules/route.ts b/src/app/api/portal/rules/route.ts index 64f787ad..2c0c0b9f 100644 --- a/src/app/api/portal/rules/route.ts +++ b/src/app/api/portal/rules/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, houseRule, ruleAcknowledgement } from '@/lib/db' +import { and, eq, inArray, or } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { logger } from '@/lib/logger' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -65,23 +66,35 @@ export async function POST(request: NextRequest) { // Only rules that actually bind this resident — an acknowledgement of // another house's rule would be meaningless and is silently impossible. - const bindingRules = await prisma.houseRule.findMany({ - where: { - id: { in: parsed.data.ruleIds }, - status: 'ACTIVE', - OR: [{ scope: 'ORG' }, { scope: 'UNIT', housingUnitId: auth.placement.housingUnitId }], - }, - select: { id: true, version: true }, + const bindingRules = await db.query.houseRule.findMany({ + where: and( + inArray(houseRule.id, parsed.data.ruleIds), + eq(houseRule.status, 'ACTIVE'), + or( + eq(houseRule.scope, 'ORG'), + and( + eq(houseRule.scope, 'UNIT'), + eq(houseRule.housingUnitId, auth.placement.housingUnitId), + ), + ), + ), + columns: { id: true, version: true }, }) - await prisma.ruleAcknowledgement.createMany({ - data: bindingRules.map((rule) => ({ - ruleId: rule.id, - residentId: auth.resident.id, - ruleVersion: rule.version, - })), - skipDuplicates: true, - }) + // None binding is a valid outcome (Prisma's createMany no-oped on an empty + // list; drizzle's `.values([])` throws). + if (bindingRules.length > 0) { + await db + .insert(ruleAcknowledgement) + .values( + bindingRules.map((rule) => ({ + ruleId: rule.id, + residentId: auth.resident.id, + ruleVersion: rule.version, + })), + ) + .onConflictDoNothing() + } return NextResponse.json({ success: true, data: { acknowledged: bindingRules.length } }) } catch (error) { diff --git a/src/app/api/portal/satisfaction/route.ts b/src/app/api/portal/satisfaction/route.ts index bac5b071..47882e8e 100644 --- a/src/app/api/portal/satisfaction/route.ts +++ b/src/app/api/portal/satisfaction/route.ts @@ -1,4 +1,11 @@ -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + satisfactionCheckIn, + incident, +} from '@/lib/db' +import { desc, eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { portalSatisfactionSchema } from '@/lib/validation/schemas' import { logger } from '@/lib/logger' @@ -30,13 +37,13 @@ export async function POST(request: NextRequest) { const { rating, concerns } = parsed.data // Find resident and their active placement - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, - include: { housingUnit: { select: { code: true } } }, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, + with: { housingUnit: { columns: { code: true } } }, }, }, }) @@ -60,45 +67,41 @@ export async function POST(request: NextRequest) { const weeksSinceStart = weeksBetween(placement.startDate) // Wrap all DB writes in a transaction - await prisma.$transaction(async (tx) => { - await tx.satisfactionCheckIn.create({ - data: { - placementId: placement.id, - checkInType: 'AD_HOC', - weekNumber: weeksSinceStart, - overallSatisfaction: rating, - roommateRelations: null, - facilitySatisfaction: null, - safetyFeeling: null, - concerns: concerns || null, - improvements: null, - positives: null, - collectedBy: null, - isAnonymous: true, - }, + await db.transaction(async (tx) => { + await tx.insert(satisfactionCheckIn).values({ + placementId: placement.id, + checkInType: 'AD_HOC', + weekNumber: weeksSinceStart, + overallSatisfaction: rating, + roommateRelations: null, + facilitySatisfaction: null, + safetyFeeling: null, + concerns: concerns || null, + improvements: null, + positives: null, + collectedBy: null, + isAnonymous: true, }) - await tx.placement.update({ - where: { id: placement.id }, - data: { + await tx + .update(placementTable) + .set({ satisfactionRating: rating, - }, - }) + }) + .where(eq(placementTable.id, placement.id)) // Create alert for staff if low rating if (rating <= 2) { - await tx.incident.create({ - data: { - housingUnitId: placement.housingUnitId, - reportedById: resident.id, - date: new Date(), - category: 'WELLBEING', - type: 'LOW_SATISFACTION', - severity: rating === 1 ? 'HIGH' : 'MEDIUM', - description: concerns - ? `Bewohner hat niedrige Zufriedenheit gemeldet: "${concerns}"` - : 'Bewohner hat niedrige Zufriedenheit im Portal gemeldet (keine Details angegeben)', - }, + await tx.insert(incident).values({ + housingUnitId: placement.housingUnitId, + reportedById: resident.id, + date: new Date(), + category: 'WELLBEING', + type: 'LOW_SATISFACTION', + severity: rating === 1 ? 'HIGH' : 'MEDIUM', + description: concerns + ? `Bewohner hat niedrige Zufriedenheit gemeldet: "${concerns}"` + : 'Bewohner hat niedrige Zufriedenheit im Portal gemeldet (keine Details angegeben)', }) } }) @@ -134,16 +137,16 @@ export async function GET() { ) } - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, - include: { + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, + with: { checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, }, }, }, diff --git a/src/app/api/portal/settlements/route.ts b/src/app/api/portal/settlements/route.ts index 0899205c..0122af7e 100644 --- a/src/app/api/portal/settlements/route.ts +++ b/src/app/api/portal/settlements/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, settlement as settlementTable } from '@/lib/db' import { getPortalAuth, getActiveUnitMembers } from '@/lib/portal-auth' import { CreateSettlementSchema } from '@/lib/validation/expenses' import { logAudit } from '@/lib/audit' @@ -41,15 +41,16 @@ export async function POST(request: NextRequest) { ) } - const settlement = await prisma.settlement.create({ - data: { + const [settlement] = await db + .insert(settlementTable) + .values({ housingUnitId: auth.placement.housingUnitId, fromId: auth.resident.id, toId: data.toResidentId, amountRappen: data.amountRappen, note: data.note || null, - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', diff --git a/src/app/api/portal/transfer/route.ts b/src/app/api/portal/transfer/route.ts index ff1b0f1a..c883f46c 100644 --- a/src/app/api/portal/transfer/route.ts +++ b/src/app/api/portal/transfer/route.ts @@ -1,4 +1,11 @@ -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + transferRequest as transferRequestTable, + housingUnit, +} from '@/lib/db' +import { eq } from 'drizzle-orm' import { NextRequest, NextResponse } from 'next/server' import { CreateTransferRequestSchema } from '@/lib/validation/transfer' import { logAudit } from '@/lib/audit' @@ -29,11 +36,11 @@ export async function POST(request: NextRequest) { const { reason, targetUnitId } = parsed.data // Find resident and active placement - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { - placements: { where: { status: 'ACTIVE' }, take: 1 }, - transferRequests: { where: { status: 'PENDING' }, take: 1 }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { + placements: { where: eq(placementTable.status, 'ACTIVE'), limit: 1 }, + transferRequests: { where: eq(transferRequestTable.status, 'PENDING'), limit: 1 }, }, }) @@ -61,14 +68,15 @@ export async function POST(request: NextRequest) { } try { - const transferRequest = await prisma.transferRequest.create({ - data: { + const [transferRequest] = await db + .insert(transferRequestTable) + .values({ residentId: resident.id, currentPlacementId: placement.id, targetUnitId: targetUnitId || null, reason, - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', @@ -83,18 +91,18 @@ export async function POST(request: NextRequest) { let currentUnitCode = 'Unbekannt' if (placement.housingUnitId) { - const unit = await prisma.housingUnit.findUnique({ - where: { id: placement.housingUnitId }, - select: { code: true }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, placement.housingUnitId), + columns: { code: true }, }) if (unit) currentUnitCode = unit.code } let targetUnitCode: string | undefined if (targetUnitId) { - const target = await prisma.housingUnit.findUnique({ - where: { id: targetUnitId }, - select: { code: true }, + const target = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, targetUnitId), + columns: { code: true }, }) if (target) targetUnitCode = target.code } diff --git a/src/app/api/residents/[id]/documents/[documentId]/route.ts b/src/app/api/residents/[id]/documents/[documentId]/route.ts index a2cb878d..e62edff3 100644 --- a/src/app/api/residents/[id]/documents/[documentId]/route.ts +++ b/src/app/api/residents/[id]/documents/[documentId]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { prisma } from '@/lib/db' +import { db, residentDocument } from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { getCurrentUser } from '@/lib/auth' import { getPortalResident } from '@/lib/portal-auth' import { hasPermission, isStaffRole } from '@/lib/auth/role-policy' @@ -63,12 +64,16 @@ export async function GET( return notFound } - const document = await prisma.residentDocument.findFirst({ + const document = await db.query.residentDocument.findFirst({ // Matched on BOTH ids: without the residentId clause a valid document id // would serve under any resident's path, and the URL would stop meaning // what it says. - where: { id: params.documentId, residentId: params.id }, - select: { fileName: true, mimeType: true, blob: { select: { data: true } } }, + where: and( + eq(residentDocument.id, params.documentId), + eq(residentDocument.residentId, params.id), + ), + columns: { fileName: true, mimeType: true }, + with: { blob: { columns: { data: true } } }, }) if (!document?.blob) return notFound diff --git a/src/app/portal/chores/[id]/page.tsx b/src/app/portal/chores/[id]/page.tsx index 735bc6c9..69d6e51c 100644 --- a/src/app/portal/chores/[id]/page.tsx +++ b/src/app/portal/chores/[id]/page.tsx @@ -1,4 +1,13 @@ -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + householdTask, + taskCompletion, + taskAttentionFlag, + taskRequest, +} from '@/lib/db' +import { eq, and, desc } from 'drizzle-orm' import { redirect, notFound } from 'next/navigation' import Link from 'next/link' import { ChoreActions } from '@/components/portal/ChoreActions' @@ -29,12 +38,12 @@ export default async function ChoreDetailPage({ params }: PageProps) { const { id } = await params const residentCode = await requireResidentCookie('/portal') - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, }, }, }) @@ -46,45 +55,46 @@ export default async function ChoreDetailPage({ params }: PageProps) { const placement = resident.placements[0] // task and roommatePlacements both depend only on placement — fetch in parallel - const [task, roommatePlacements] = await Promise.all([ - prisma.householdTask.findFirst({ - where: { - id, - housingUnitId: placement.housingUnitId, - }, - include: { - // Whose turn it is counts EVERY completion ever, not the page's - // truncated history slice — a rota that silently reset every 20 - // completions would put the same person up twice in a row. - _count: { select: { completions: true } }, - createdByResident: { select: RESIDENT_NAME_SELECT }, + const [task, roommatePlacements, totalCompletionCount] = await Promise.all([ + db.query.householdTask.findFirst({ + where: and( + eq(householdTask.id, id), + eq(householdTask.housingUnitId, placement.housingUnitId), + ), + with: { + createdByResident: { columns: RESIDENT_NAME_SELECT }, completions: { - orderBy: { completedAt: 'desc' }, - take: QUERY_LIMITS.choreHistory, - include: { completedBy: { select: RESIDENT_NAME_SELECT } }, + orderBy: [desc(taskCompletion.completedAt)], + limit: QUERY_LIMITS.choreHistory, + with: { completedBy: { columns: RESIDENT_NAME_SELECT } }, }, attentionFlags: { - orderBy: { createdAt: 'desc' }, - include: { flaggedBy: { select: RESIDENT_NAME_SELECT } }, + orderBy: [desc(taskAttentionFlag.createdAt)], + with: { flaggedBy: { columns: RESIDENT_NAME_SELECT } }, }, requests: { - orderBy: { createdAt: 'desc' }, - include: { - requestedBy: { select: RESIDENT_NAME_SELECT }, - requestedResident: { select: RESIDENT_NAME_SELECT }, + orderBy: [desc(taskRequest.createdAt)], + with: { + requestedBy: { columns: RESIDENT_NAME_SELECT }, + requestedResident: { columns: RESIDENT_NAME_SELECT }, }, }, }, }), - prisma.placement.findMany({ - where: { - housingUnitId: placement.housingUnitId, - status: 'ACTIVE', - }, - select: { - resident: { select: RESIDENT_NAME_SELECT }, + db.query.placement.findMany({ + where: and( + eq(placementTable.housingUnitId, placement.housingUnitId), + eq(placementTable.status, 'ACTIVE'), + ), + columns: {}, + with: { + resident: { columns: RESIDENT_NAME_SELECT }, }, }), + // Whose turn it is counts EVERY completion ever, not the page's + // truncated history slice — a rota that silently reset every 20 + // completions would put the same person up twice in a row. + db.$count(taskCompletion, eq(taskCompletion.taskId, id)), ]) if (!task) { @@ -95,7 +105,10 @@ export default async function ChoreDetailPage({ params }: PageProps) { // The request dropdown asks someone ELSE to take this on. const roommates = household.filter((r) => r.id !== resident.id) - const turnResidentId = currentTurnResidentId(task.rotationResidentIds, task._count.completions) + // `text[]` columns type as nullable at the DB level even though the app + // always writes arrays — normalize once so the render logic stays simple. + const checklist = task.checklist ?? [] + const turnResidentId = currentTurnResidentId(task.rotationResidentIds ?? [], totalCompletionCount) const turnResident = household.find((r) => r.id === turnResidentId) const isMyTurn = turnResidentId === resident.id @@ -159,12 +172,12 @@ export default async function ChoreDetailPage({ params }: PageProps) { hiding it makes the gap invisible. */}

{CHORE_LABELS.detail.checklist}

- {task.checklist.length === 0 ? ( + {checklist.length === 0 ? (

{CHORE_LABELS.detail.noChecklist}

) : ( <>
    - {task.checklist.map((item) => ( + {checklist.map((item) => (
@@ -242,7 +255,7 @@ export default async function ChoreDetailPage({ params }: PageProps) { {/* What was actually ticked. A partial completion stays visibly partial instead of collapsing into "erledigt", which is what lets the next person see what was left. */} - {c.completedItems.length > 0 && ( + {c.completedItems && c.completedItems.length > 0 && (
    {c.completedItems.map((item) => (
  • diff --git a/src/app/portal/chores/new/page.tsx b/src/app/portal/chores/new/page.tsx index b6d903b6..c155d586 100644 --- a/src/app/portal/chores/new/page.tsx +++ b/src/app/portal/chores/new/page.tsx @@ -1,5 +1,6 @@ import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement as placementTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { CreateChoreForm } from '@/components/portal/CreateChoreForm' import { CHORE_LABELS } from '@/lib/config/household-tasks' import { requireResidentCookie } from '@/lib/portal-auth' @@ -10,12 +11,12 @@ export const dynamic = 'force-dynamic' export default async function NewChorePage() { const residentCode = await requireResidentCookie('/portal') - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, }, }, }) diff --git a/src/app/portal/chores/page.tsx b/src/app/portal/chores/page.tsx index 5ea48975..9ff01934 100644 --- a/src/app/portal/chores/page.tsx +++ b/src/app/portal/chores/page.tsx @@ -1,6 +1,15 @@ import type { Metadata } from 'next' import { getRequestTranslator } from '@/lib/i18n/request' -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + householdTask, + taskCompletion, + taskAttentionFlag, + taskRequest, +} from '@/lib/db' +import { eq, desc, inArray } from 'drizzle-orm' import { redirect } from 'next/navigation' import Link from 'next/link' @@ -19,12 +28,12 @@ export const dynamic = 'force-dynamic' export default async function ChoresPage() { const residentCode = await requireResidentCookie('/portal') - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, }, }, }) @@ -47,23 +56,27 @@ export default async function ChoresPage() { // tasks and balances both only need placement.housingUnitId — fetch in parallel const [tasks, balances] = await Promise.all([ - prisma.householdTask.findMany({ - where: { housingUnitId: placement.housingUnitId }, - include: { + db.query.householdTask.findMany({ + where: eq(householdTask.housingUnitId, placement.housingUnitId), + with: { completions: { - orderBy: { completedAt: 'desc' }, - take: 1, - include: { completedBy: { select: RESIDENT_NAME_SELECT } }, + orderBy: [desc(taskCompletion.completedAt)], + limit: 1, + with: { completedBy: { columns: RESIDENT_NAME_SELECT } }, }, attentionFlags: { - where: { isResolved: false }, + where: eq(taskAttentionFlag.isResolved, false), }, requests: { - where: { status: { in: ['PENDING', 'ACCEPTED'] } }, + where: inArray(taskRequest.status, ['PENDING', 'ACCEPTED']), }, - createdByResident: { select: RESIDENT_NAME_SELECT }, + createdByResident: { columns: RESIDENT_NAME_SELECT }, }, - orderBy: [{ currentStatus: 'desc' }, { priority: 'desc' }, { createdAt: 'desc' }], + orderBy: [ + desc(householdTask.currentStatus), + desc(householdTask.priority), + desc(householdTask.createdAt), + ], }), loadChoreBalances(placement.housingUnitId), ]) diff --git a/src/app/portal/decisions/page.tsx b/src/app/portal/decisions/page.tsx index aa6ced14..219c917f 100644 --- a/src/app/portal/decisions/page.tsx +++ b/src/app/portal/decisions/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement as placementTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requireResidentCookie } from '@/lib/portal-auth' import { getRuleBook, getUnitProposals, getUnitResidentIds } from '@/lib/governance/queries' import { advanceDueProposals } from '@/lib/governance/lifecycle' @@ -22,9 +23,9 @@ export default async function PortalDecisionsPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { placements: { where: { status: 'ACTIVE' }, take: 1 } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { where: eq(placementTable.status, 'ACTIVE'), limit: 1 } }, }) if (!resident) redirect('/portal') diff --git a/src/app/portal/housing/page.tsx b/src/app/portal/housing/page.tsx index cf469dd2..d0f4698c 100644 --- a/src/app/portal/housing/page.tsx +++ b/src/app/portal/housing/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement, housingUnit, placementSpot } from '@/lib/db' +import { eq, and, ne, asc } from 'drizzle-orm' import { toResidentProfile } from '@/lib/compatibility/convert' import { calculateApartmentProfile, calculateApartmentFit } from '@/lib/compatibility/aggregate' import { PortalHousingBrowse } from '@/components/portal/PortalHousingBrowse' @@ -18,8 +19,8 @@ export default async function PortalHousingPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), }) if (!resident) { @@ -32,26 +33,26 @@ export default async function PortalHousingPage() { } // Check if already placed — redirect to dashboard - const activePlacement = await prisma.placement.findFirst({ - where: { residentId: resident.id, status: 'ACTIVE' }, + const activePlacement = await db.query.placement.findFirst({ + where: and(eq(placement.residentId, resident.id), eq(placement.status, 'ACTIVE')), }) if (activePlacement) { redirect('/portal') } // Fetch available housing units with current residents and available spots - const units = await prisma.housingUnit.findMany({ - where: { status: 'AVAILABLE' }, - include: { + const units = await db.query.housingUnit.findMany({ + where: eq(housingUnit.status, 'AVAILABLE'), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { resident: true }, + where: eq(placement.status, 'ACTIVE'), + with: { resident: true }, }, spots: { - where: { status: 'AVAILABLE', type: { not: 'ROOM' } }, + where: and(eq(placementSpot.status, 'AVAILABLE'), ne(placementSpot.type, 'ROOM')), }, }, - orderBy: { code: 'asc' }, + orderBy: [asc(housingUnit.code)], }) const residentProfile = toResidentProfile(resident) diff --git a/src/app/portal/layout.tsx b/src/app/portal/layout.tsx index 9f8c05d1..d1729eb8 100644 --- a/src/app/portal/layout.tsx +++ b/src/app/portal/layout.tsx @@ -10,7 +10,8 @@ import { PortalSidebar } from '@/components/portal/PortalSidebar' import { PortalTabBar } from '@/components/portal/PortalTabBar' import { BRAND } from '@/lib/config/brand' import { RESIDENT_COOKIE, STAFF_COOKIE } from '@/lib/auth/constants' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { residentUnreadCount } from '@/lib/messaging/queries' /** @@ -46,9 +47,9 @@ export default async function PortalLayout({ children }: { children: React.React const residentCode = cookieStore.get(RESIDENT_COOKIE)?.value const hasStaffAccess = !!cookieStore.get(STAFF_COOKIE)?.value const resident = residentCode - ? await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true }, + ? await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + columns: { id: true }, }) : null const messageUnreadCount = resident ? await residentUnreadCount(resident.id) : 0 diff --git a/src/app/portal/messages/page.tsx b/src/app/portal/messages/page.tsx index bfaf289a..24c6589e 100644 --- a/src/app/portal/messages/page.tsx +++ b/src/app/portal/messages/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from 'next' import { getRequestTranslator } from '@/lib/i18n/request' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requireResidentCookie } from '@/lib/portal-auth' import { PageHeader } from '@/components/ui/Page' import { MessageThreadView } from '@/components/portal/MessageThread' @@ -27,9 +28,9 @@ export const dynamic = 'force-dynamic' export default async function PortalMessagesPage() { const residentCode = await requireResidentCookie('/portal') - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + columns: { id: true }, }) if (!resident) redirect('/portal?error=account_not_found') diff --git a/src/app/portal/page.tsx b/src/app/portal/page.tsx index 03269d83..ece3bb8c 100644 --- a/src/app/portal/page.tsx +++ b/src/app/portal/page.tsx @@ -1,6 +1,16 @@ import type { Metadata } from 'next' import { getRequestTranslator } from '@/lib/i18n/request' -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement, + maintenanceRequest, + satisfactionCheckIn, + incident, + householdTask, + compatibilityAssessment, +} from '@/lib/db' +import { eq, and, or, desc, inArray, notInArray } from 'drizzle-orm' import { redirect } from 'next/navigation' // The portal's name is the brand's decision, not this file's — the tab used to @@ -40,47 +50,49 @@ export default async function ResidentPortal() { const residentCode = await requireResidentCookie('/login') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { - photo: { select: { updatedAt: true } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { + photo: { columns: { updatedAt: true } }, placements: { - where: { status: 'ACTIVE' }, - include: { + where: eq(placement.status, 'ACTIVE'), + with: { housingUnit: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { + where: eq(placement.status, 'ACTIVE'), + with: { resident: { - select: { + columns: { id: true, code: true, displayName: true, socialStyle: true, - photo: { select: { updatedAt: true } }, + }, + with: { + photo: { columns: { updatedAt: true } }, }, }, }, }, maintenanceRequests: { - where: { status: { notIn: ['COMPLETED', 'CANCELLED'] } }, - orderBy: { createdAt: 'desc' }, - take: DISPLAY_LIMITS.portalIncidentPreview, - select: { id: true, category: true, createdAt: true }, + where: notInArray(maintenanceRequest.status, ['COMPLETED', 'CANCELLED']), + orderBy: [desc(maintenanceRequest.createdAt)], + limit: DISPLAY_LIMITS.portalIncidentPreview, + columns: { id: true, category: true, createdAt: true }, }, }, }, checkIns: { - orderBy: { createdAt: 'desc' }, - take: 1, + orderBy: [desc(satisfactionCheckIn.createdAt)], + limit: 1, }, }, }, incidentsReported: { - orderBy: { date: 'desc' }, - take: DISPLAY_LIMITS.portalIncidentPreview, - select: { + orderBy: [desc(incident.date)], + limit: DISPLAY_LIMITS.portalIncidentPreview, + columns: { id: true, type: true, description: true, @@ -92,9 +104,9 @@ export default async function ResidentPortal() { // A resident's own reports live in two tables — conflicts on the ladder, // broken things on the maintenance board — but they are one list to them. maintenanceRequests: { - orderBy: { createdAt: 'desc' }, - take: DISPLAY_LIMITS.portalIncidentPreview, - select: { + orderBy: [desc(maintenanceRequest.createdAt)], + limit: DISPLAY_LIMITS.portalIncidentPreview, + columns: { id: true, category: true, description: true, @@ -133,26 +145,36 @@ export default async function ResidentPortal() { myMarketplacePosts, ] = await Promise.all([ currentPlacement - ? prisma.householdTask.findMany({ - where: { - housingUnitId: currentPlacement.housingUnitId, - isCompleted: false, - currentStatus: { in: ['NEEDS_ATTENTION', 'REQUESTED'] }, - }, - select: { id: true, title: true, currentStatus: true }, - orderBy: { updatedAt: 'desc' }, - take: DISPLAY_LIMITS.dashboardItems, + ? db.query.householdTask.findMany({ + where: and( + eq(householdTask.housingUnitId, currentPlacement.housingUnitId), + eq(householdTask.isCompleted, false), + inArray(householdTask.currentStatus, ['NEEDS_ATTENTION', 'REQUESTED']), + ), + columns: { id: true, title: true, currentStatus: true }, + orderBy: [desc(householdTask.updatedAt)], + limit: DISPLAY_LIMITS.dashboardItems, }) : Promise.resolve([]), roommates.length > 0 - ? prisma.compatibilityAssessment.findMany({ - where: { - OR: [ - { residentId: resident.id, comparedWithId: { in: roommates.map((r) => r.id) } }, - { residentId: { in: roommates.map((r) => r.id) }, comparedWithId: resident.id }, - ], - }, - select: { + ? db.query.compatibilityAssessment.findMany({ + where: or( + and( + eq(compatibilityAssessment.residentId, resident.id), + inArray( + compatibilityAssessment.comparedWithId, + roommates.map((r) => r.id), + ), + ), + and( + inArray( + compatibilityAssessment.residentId, + roommates.map((r) => r.id), + ), + eq(compatibilityAssessment.comparedWithId, resident.id), + ), + ), + columns: { residentId: true, comparedWithId: true, overallScore: true, diff --git a/src/app/portal/preferences/page.tsx b/src/app/portal/preferences/page.tsx index 19a7b3a8..8ef11b72 100644 --- a/src/app/portal/preferences/page.tsx +++ b/src/app/portal/preferences/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { getRequestTranslator } from '@/lib/i18n/request' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { redirect } from 'next/navigation' export async function generateMetadata(): Promise { @@ -25,8 +26,8 @@ export default async function PreferencesPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), }) if (!resident) { @@ -57,8 +58,8 @@ export default async function PreferencesPage() { petTolerance: resident.petTolerance, sharedBathroom: resident.sharedBathroom, sharedKitchen: resident.sharedKitchen, - languages: resident.languages, - dietaryNeeds: resident.dietaryNeeds, + languages: resident.languages ?? [], + dietaryNeeds: resident.dietaryNeeds ?? [], roommatePreferences: resident.roommatePreferences, }} languageOptions={[...LANGUAGE_OPTIONS]} diff --git a/src/app/portal/profile/page.tsx b/src/app/portal/profile/page.tsx index b64eab9a..cc9d779c 100644 --- a/src/app/portal/profile/page.tsx +++ b/src/app/portal/profile/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requireResidentCookie } from '@/lib/portal-auth' import { ProfileForm } from '@/components/portal/ProfileForm' import { PhotoUploader } from '@/components/portal/PhotoUploader' @@ -23,15 +24,17 @@ export default async function PortalProfilePage() { const { t } = await getRequestTranslator() const L = buildProfileLabels(t) - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + columns: { id: true, code: true, displayName: true, bio: true, profileVisibility: true, - photo: { select: { updatedAt: true } }, + }, + with: { + photo: { columns: { updatedAt: true } }, }, }) if (!resident) redirect('/login') diff --git a/src/app/portal/report/page.tsx b/src/app/portal/report/page.tsx index d73d056d..c9e76570 100644 --- a/src/app/portal/report/page.tsx +++ b/src/app/portal/report/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { getRequestTranslator } from '@/lib/i18n/request' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement } from '@/lib/db' +import { eq } from 'drizzle-orm' import { redirect } from 'next/navigation' export async function generateMetadata(): Promise { @@ -19,19 +20,19 @@ export default async function ReportPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - include: { + where: eq(placement.status, 'ACTIVE'), + with: { housingUnit: { - include: { + with: { placements: { - where: { status: 'ACTIVE' }, - include: { + where: eq(placement.status, 'ACTIVE'), + with: { resident: { - select: RESIDENT_NAME_SELECT, + columns: RESIDENT_NAME_SELECT, }, }, }, diff --git a/src/app/portal/reports/page.tsx b/src/app/portal/reports/page.tsx index 48d3b019..2c107f51 100644 --- a/src/app/portal/reports/page.tsx +++ b/src/app/portal/reports/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from 'next' import Link from 'next/link' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, incident, maintenanceRequest } from '@/lib/db' +import { eq, desc } from 'drizzle-orm' import { requireResidentCookie } from '@/lib/portal-auth' import { mergeResidentReports } from '@/lib/reports/resident-reports' import { getRequestTranslator } from '@/lib/i18n/request' @@ -31,13 +32,13 @@ export default async function PortalReportsPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { - id: true, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + columns: { id: true }, + with: { incidentsReported: { - orderBy: { date: 'desc' }, - select: { + orderBy: [desc(incident.date)], + columns: { id: true, type: true, description: true, @@ -47,8 +48,8 @@ export default async function PortalReportsPage() { }, }, maintenanceRequests: { - orderBy: { createdAt: 'desc' }, - select: { + orderBy: [desc(maintenanceRequest.createdAt)], + columns: { id: true, category: true, description: true, diff --git a/src/app/portal/rules/page.tsx b/src/app/portal/rules/page.tsx index d1b6da97..6af9fa60 100644 --- a/src/app/portal/rules/page.tsx +++ b/src/app/portal/rules/page.tsx @@ -1,7 +1,8 @@ import type { Metadata } from 'next' import { redirect } from 'next/navigation' import Link from 'next/link' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, placement as placementTable } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requireResidentCookie } from '@/lib/portal-auth' import { getOutstandingRules, getRuleBook } from '@/lib/governance/queries' import { AcknowledgeRulesPanel } from '@/components/governance/AcknowledgeRulesPanel' @@ -20,9 +21,9 @@ export default async function PortalRulesPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { placements: { where: { status: 'ACTIVE' }, take: 1 } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { where: eq(placementTable.status, 'ACTIVE'), limit: 1 } }, }) if (!resident) redirect('/portal') diff --git a/src/app/portal/transfer/page.tsx b/src/app/portal/transfer/page.tsx index 6d562af0..86e4288b 100644 --- a/src/app/portal/transfer/page.tsx +++ b/src/app/portal/transfer/page.tsx @@ -1,5 +1,12 @@ import type { Metadata } from 'next' -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement as placementTable, + transferRequest, + housingUnit, +} from '@/lib/db' +import { eq, and, ne, desc, asc } from 'drizzle-orm' import { redirect } from 'next/navigation' import { TransferRequestForm } from '@/components/portal/TransferRequestForm' import { buildTransferLabels } from '@/lib/i18n/portal-surfaces' @@ -20,14 +27,14 @@ export default async function TransferPage() { const residentCode = await requireResidentCookie('/portal') const { t } = await getRequestTranslator() - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - include: { + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, residentCode), + with: { placements: { - where: { status: 'ACTIVE' }, - take: 1, - include: { - housingUnit: { select: { id: true, code: true, address: true } }, + where: eq(placementTable.status, 'ACTIVE'), + limit: 1, + with: { + housingUnit: { columns: { id: true, code: true, address: true } }, }, }, // The LATEST request, whatever its status — not just the pending one. @@ -35,9 +42,9 @@ export default async function TransferPage() { // resident's view the moment it was made, taking the staff note with // it: they were told "du wirst benachrichtigt" and then never were. transferRequests: { - orderBy: { createdAt: 'desc' }, - take: 1, - select: { + orderBy: [desc(transferRequest.createdAt)], + limit: 1, + columns: { id: true, createdAt: true, reason: true, @@ -62,13 +69,13 @@ export default async function TransferPage() { // Fetch available units for optional target selection (exclude current unit) const availableUnits = placement - ? await prisma.housingUnit.findMany({ - where: { - status: 'AVAILABLE', - id: { not: placement.housingUnit?.id }, - }, - select: { id: true, code: true, address: true }, - orderBy: { code: 'asc' }, + ? await db.query.housingUnit.findMany({ + where: and( + eq(housingUnit.status, 'AVAILABLE'), + ne(housingUnit.id, placement.housingUnit.id), + ), + columns: { id: true, code: true, address: true }, + orderBy: [asc(housingUnit.code)], }) : [] diff --git a/src/components/governance/AgreementsPanel.tsx b/src/components/governance/AgreementsPanel.tsx index 4463db9a..19891bb4 100644 --- a/src/components/governance/AgreementsPanel.tsx +++ b/src/components/governance/AgreementsPanel.tsx @@ -2,7 +2,7 @@ import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' -import type { AgreementStatus } from '@prisma/client' +import type { AgreementStatus } from '@/lib/db' import { createAgreement, reviewAgreement } from '@/lib/actions/governance' import { AGREEMENT_QUALITY_HINTS, diff --git a/src/components/governance/NewProposalForm.tsx b/src/components/governance/NewProposalForm.tsx index cc9c3654..9a0a3480 100644 --- a/src/components/governance/NewProposalForm.tsx +++ b/src/components/governance/NewProposalForm.tsx @@ -2,7 +2,7 @@ import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' -import type { RuleCategory, RuleDelegation, ProposalType } from '@prisma/client' +import type { RuleCategory, RuleDelegation, ProposalType } from '@/lib/db' import { RULE_CATEGORY_LABELS } from '@/lib/config/house-rules' import { CATEGORY_DECISION_MODE, diff --git a/src/components/governance/ProposalList.tsx b/src/components/governance/ProposalList.tsx index 129f24ab..a908f30f 100644 --- a/src/components/governance/ProposalList.tsx +++ b/src/components/governance/ProposalList.tsx @@ -9,7 +9,7 @@ import type { RuleCategory, VoteChoice, VoteThreshold, -} from '@prisma/client' +} from '@/lib/db' import { DECISION_MODE_IS_VOTED, DECISION_MODE_LABELS, diff --git a/src/components/governance/ResolutionLadder.tsx b/src/components/governance/ResolutionLadder.tsx index 5056d866..bda68d73 100644 --- a/src/components/governance/ResolutionLadder.tsx +++ b/src/components/governance/ResolutionLadder.tsx @@ -2,7 +2,7 @@ import { useState, useTransition } from 'react' import { useRouter } from 'next/navigation' -import type { ResolutionStage } from '@prisma/client' +import type { ResolutionStage } from '@/lib/db' import { advanceResolutionStage } from '@/lib/actions/governance' import { RESOLUTION_LADDER, diff --git a/src/components/housing/CompatibilityDetailPopover.tsx b/src/components/housing/CompatibilityDetailPopover.tsx index d8447167..b6000014 100644 --- a/src/components/housing/CompatibilityDetailPopover.tsx +++ b/src/components/housing/CompatibilityDetailPopover.tsx @@ -23,7 +23,7 @@ export interface CompatibilityScore { practicalScore?: number riskScore?: number conflicts?: { message: string; severity: string }[] - strengths?: string[] + strengths?: string[] | null } export interface CompatibilityDetailPopoverProps { diff --git a/src/components/incidents/FollowUpTimeline.tsx b/src/components/incidents/FollowUpTimeline.tsx index dd51696b..64c771d4 100644 --- a/src/components/incidents/FollowUpTimeline.tsx +++ b/src/components/incidents/FollowUpTimeline.tsx @@ -1,4 +1,4 @@ -import type { IncidentFollowUp } from '@prisma/client' +import type { IncidentFollowUp } from '@/lib/db' import { formatRelativeDate, formatDate } from '@/lib/utils' import { INCIDENT_DETAIL_LABELS } from '@/lib/constants' diff --git a/src/components/learning/IntegrationBoard.tsx b/src/components/learning/IntegrationBoard.tsx index 01eb4863..c2f0dbb8 100644 --- a/src/components/learning/IntegrationBoard.tsx +++ b/src/components/learning/IntegrationBoard.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { LearningRecord, ResidentOrStaff } from '@prisma/client' +import type { LearningRecord, ResidentOrStaff } from '@/lib/db' import { LEARNING_CATEGORY_LABELS, LEARNING_KIND_LABELS, diff --git a/src/components/matching/ApartmentProfileSection.tsx b/src/components/matching/ApartmentProfileSection.tsx index 8208a351..71b554ac 100644 --- a/src/components/matching/ApartmentProfileSection.tsx +++ b/src/components/matching/ApartmentProfileSection.tsx @@ -1,4 +1,4 @@ -import type { Resident, Placement } from '@prisma/client' +import type { Resident, Placement } from '@/lib/db' import type { ApartmentConflict, ApartmentProfile, diff --git a/src/components/matching/HeadToHeadComparison.tsx b/src/components/matching/HeadToHeadComparison.tsx index f336831d..5576b2f5 100644 --- a/src/components/matching/HeadToHeadComparison.tsx +++ b/src/components/matching/HeadToHeadComparison.tsx @@ -1,4 +1,4 @@ -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ApartmentProfile } from '@/lib/compatibility/types' import { SLEEP_SCHEDULE_LABELS_SHORT, diff --git a/src/components/matching/MatchCard.tsx b/src/components/matching/MatchCard.tsx index b1e8f166..099e45d8 100644 --- a/src/components/matching/MatchCard.tsx +++ b/src/components/matching/MatchCard.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ApartmentConflict } from '@/lib/compatibility/types' import type { MatchResult, CompatibilityDetail } from '@/lib/matching/types' import { placeResident } from '@/lib/actions/matching' diff --git a/src/components/matching/ResidentSelectorPanel.tsx b/src/components/matching/ResidentSelectorPanel.tsx index 3ea80cb4..a117a0c8 100644 --- a/src/components/matching/ResidentSelectorPanel.tsx +++ b/src/components/matching/ResidentSelectorPanel.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ResidentWithPlacement } from '@/lib/matching/types' import { AGE_RANGE_LABELS, @@ -92,7 +92,7 @@ export function ResidentSelectorPanel({

    {getLabel(AGE_RANGE_LABELS, resident.ageRange)} ·{' '} - {resident.languages + {(resident.languages ?? []) .slice(0, DISPLAY_LIMITS.languagePreview) .map((l) => getLabel(LANGUAGE_LABELS, l)) .join(', ')} diff --git a/src/components/matching/SpotSelection.tsx b/src/components/matching/SpotSelection.tsx index 1685597f..84261fbc 100644 --- a/src/components/matching/SpotSelection.tsx +++ b/src/components/matching/SpotSelection.tsx @@ -1,4 +1,4 @@ -import type { Resident, Placement, PlacementSpot } from '@prisma/client' +import type { Resident, Placement, PlacementSpot } from '@/lib/db' import type { ApartmentConflict } from '@/lib/compatibility/types' import type { MatchResult } from '@/lib/matching/types' import { placeResident } from '@/lib/actions/matching' diff --git a/src/components/matching/UnitModePanel.tsx b/src/components/matching/UnitModePanel.tsx index 0e199958..dfbe05ac 100644 --- a/src/components/matching/UnitModePanel.tsx +++ b/src/components/matching/UnitModePanel.tsx @@ -85,7 +85,7 @@ export function UnitModePanel({ selectedUnit, unitMatches }: Props) {

    {getLabel(AGE_RANGE_LABELS, match.resident.ageRange)} ·{' '} - {match.resident.languages + {(match.resident.languages ?? []) .slice(0, DISPLAY_LIMITS.languagePreview) .map((l: string) => getLabel(LANGUAGE_LABELS, l)) .join(', ')} diff --git a/src/components/portal/MarketplacePostForm.tsx b/src/components/portal/MarketplacePostForm.tsx index bb56df3f..0922812f 100644 --- a/src/components/portal/MarketplacePostForm.tsx +++ b/src/components/portal/MarketplacePostForm.tsx @@ -12,7 +12,7 @@ import { natureOfKind, } from '@/lib/config/marketplace' import { useT } from '@/lib/i18n/LocaleProvider' -import type { MarketplacePostKind } from '@prisma/client' +import type { MarketplacePostKind } from '@/lib/db' /** * Posting form for the marketplace. diff --git a/src/components/portal/ProfileForm.tsx b/src/components/portal/ProfileForm.tsx index 4a7c75bb..23b45d41 100644 --- a/src/components/portal/ProfileForm.tsx +++ b/src/components/portal/ProfileForm.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { PROFILE_LIMITS } from '@/lib/config/profile' import { PROFILE_VISIBILITY_OPTIONS } from '@/lib/privacy/profile-visibility' -import type { ProfileVisibility } from '@prisma/client' +import type { ProfileVisibility } from '@/lib/db' import { useT } from '@/lib/i18n/LocaleProvider' import { buildProfileLabels } from '@/lib/i18n/portal-surfaces' diff --git a/src/components/residents/LearningRecordsCard.tsx b/src/components/residents/LearningRecordsCard.tsx index 6bd5ef09..2d87200a 100644 --- a/src/components/residents/LearningRecordsCard.tsx +++ b/src/components/residents/LearningRecordsCard.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { LearningRecord } from '@prisma/client' +import type { LearningRecord } from '@/lib/db' import { LEARNING_CATEGORY_LABELS, LEARNING_KIND_LABELS, diff --git a/src/components/residents/ResidentIncidents.tsx b/src/components/residents/ResidentIncidents.tsx index 1ad65881..a8caaca6 100644 --- a/src/components/residents/ResidentIncidents.tsx +++ b/src/components/residents/ResidentIncidents.tsx @@ -1,5 +1,5 @@ import Link from 'next/link' -import type { Incident, HousingUnit } from '@prisma/client' +import type { Incident, HousingUnit } from '@/lib/db' import { INCIDENT_TYPE_LABELS, RESIDENT_INCIDENTS_LABELS, diff --git a/src/components/residents/ResidentProfileSidebar.tsx b/src/components/residents/ResidentProfileSidebar.tsx index b413cf33..64b6350e 100644 --- a/src/components/residents/ResidentProfileSidebar.tsx +++ b/src/components/residents/ResidentProfileSidebar.tsx @@ -1,4 +1,4 @@ -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import { SLEEP_SCHEDULE_LABELS, SOCIAL_STYLE_LABELS, @@ -189,7 +189,7 @@ export function ResidentProfileSidebar({ resident }: ResidentProfileSidebarProps

    {RESIDENT_PROFILE_SIDEBAR_LABELS.fieldLanguages}
    - {resident.languages.map((lang) => ( + {(resident.languages ?? []).map((lang) => ( - {resident.dietaryNeeds.length > 0 && ( + {(resident.dietaryNeeds ?? []).length > 0 && (
    {RESIDENT_PROFILE_SIDEBAR_LABELS.fieldDiet}
    - {resident.dietaryNeeds.map((diet) => ( + {(resident.dietaryNeeds ?? []).map((diet) => ( {getLabel(DIET_LABELS, diet)} diff --git a/src/lib/actions/care.ts b/src/lib/actions/care.ts index 47f0bd15..a2dcd8dd 100644 --- a/src/lib/actions/care.ts +++ b/src/lib/actions/care.ts @@ -1,7 +1,17 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { + db, + appointment as appointmentTable, + careAssignment, + careAttribute, + placement as placementTable, + satisfactionCheckIn, + user as userTable, +} from '@/lib/db' +import type { AppointmentStatus, CareRole } from '@/lib/db' +import { and, asc, eq, gte, or } from 'drizzle-orm' import { getCurrentUser } from '@/lib/auth' import { getPortalAuth } from '@/lib/portal-auth' import { @@ -23,7 +33,6 @@ import { import { fromDatetimeLocalInput } from '@/lib/utils/local-time' import { weeksBetween } from '@/lib/utils' import { logAudit } from '@/lib/audit' -import type { AppointmentStatus, CareRole } from '@prisma/client' export type CareSeat = { role: CareRoleId @@ -70,9 +79,9 @@ export type CareAttributeValue = { * Used to power the "Meine Klient*innen" filter on the client board. */ export async function getMyResidentIds(staffId: string): Promise { - const assignments = await prisma.careAssignment.findMany({ - where: { staffId }, - select: { residentId: true }, + const assignments = await db.query.careAssignment.findMany({ + where: eq(careAssignment.staffId, staffId), + columns: { residentId: true }, }) return assignments.map((a) => a.residentId) } @@ -102,9 +111,9 @@ function revalidateResident(residentId: string) { } export async function getCareTeam(residentId: string): Promise { - const assignments = await prisma.careAssignment.findMany({ - where: { residentId }, - include: { staff: { select: { id: true, name: true } } }, + const assignments = await db.query.careAssignment.findMany({ + where: eq(careAssignment.residentId, residentId), + with: { staff: { columns: { id: true, name: true } } }, }) const byRole = new Map(assignments.map((row) => [row.role, row])) @@ -119,15 +128,17 @@ export async function getCareTeam(residentId: string): Promise { } export async function listAssignableStaff(): Promise { - return prisma.user.findMany({ - where: { active: true }, - select: { id: true, name: true, role: true }, - orderBy: { name: 'asc' }, + return db.query.user.findMany({ + where: eq(userTable.active, true), + columns: { id: true, name: true, role: true }, + orderBy: [asc(userTable.name)], }) } export async function listCareAttributes(residentId: string): Promise { - const rows = await prisma.careAttribute.findMany({ where: { residentId } }) + const rows = await db.query.careAttribute.findMany({ + where: eq(careAttribute.residentId, residentId), + }) return rows.map((row) => ({ domain: row.domain as CareRoleId, key: row.key, @@ -136,10 +147,10 @@ export async function listCareAttributes(residentId: string): Promise { - const rows = await prisma.appointment.findMany({ - where: { residentId }, - include: { staff: { select: { id: true, name: true } } }, - orderBy: { startsAt: 'asc' }, + const rows = await db.query.appointment.findMany({ + where: eq(appointmentTable.residentId, residentId), + with: { staff: { columns: { id: true, name: true } } }, + orderBy: [asc(appointmentTable.startsAt)], }) return rows.map(mapAppointment) } @@ -162,20 +173,23 @@ export async function listUpcomingResidentAppointments( ): Promise { const answeredSince = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) - const rows = await prisma.appointment.findMany({ - where: { - residentId, - OR: [ - { status: 'SCHEDULED', startsAt: { gte: now } }, + const rows = await db.query.appointment.findMany({ + where: and( + eq(appointmentTable.residentId, residentId), + or( + and(eq(appointmentTable.status, 'SCHEDULED'), gte(appointmentTable.startsAt, now)), // An open request, whenever it was for — an unanswered ask does not // stop mattering because the date the resident suggested has passed. - { status: 'REQUESTED' }, - { status: 'CANCELLED', updatedAt: { gte: answeredSince } }, - ], - }, - include: { staff: { select: { id: true, name: true } } }, - orderBy: { startsAt: 'asc' }, - take: 12, + eq(appointmentTable.status, 'REQUESTED'), + and( + eq(appointmentTable.status, 'CANCELLED'), + gte(appointmentTable.updatedAt, answeredSince), + ), + ), + ), + with: { staff: { columns: { id: true, name: true } } }, + orderBy: [asc(appointmentTable.startsAt)], + limit: 12, }) return rows.map(mapAppointment) } @@ -227,19 +241,23 @@ export async function saveCareSeat( } if (!staffId) { - await prisma.careAssignment.deleteMany({ where: { residentId, role } }) + await db + .delete(careAssignment) + .where(and(eq(careAssignment.residentId, residentId), eq(careAssignment.role, role))) } else { - const staff = await prisma.user.findFirst({ - where: { id: staffId, active: true }, - select: { id: true }, + const staff = await db.query.user.findFirst({ + where: and(eq(userTable.id, staffId), eq(userTable.active, true)), + columns: { id: true }, }) if (!staff) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } - await prisma.careAssignment.upsert({ - where: { residentId_role: { residentId, role } }, - create: { residentId, staffId, role }, - update: { staffId }, - }) + await db + .insert(careAssignment) + .values({ residentId, staffId, role }) + .onConflictDoUpdate({ + target: [careAssignment.residentId, careAssignment.role], + set: { staffId }, + }) } revalidateResident(residentId) @@ -267,14 +285,24 @@ export async function saveCareAttributes( if (!isCatalogKey(domain, key)) continue const value = String(raw).trim() if (!value) { - await prisma.careAttribute.deleteMany({ where: { residentId, domain, key } }) + await db + .delete(careAttribute) + .where( + and( + eq(careAttribute.residentId, residentId), + eq(careAttribute.domain, domain), + eq(careAttribute.key, key), + ), + ) continue } - await prisma.careAttribute.upsert({ - where: { residentId_domain_key: { residentId, domain, key } }, - create: { residentId, domain, key, value, updatedById: user.id }, - update: { value, updatedById: user.id }, - }) + await db + .insert(careAttribute) + .values({ residentId, domain, key, value, updatedById: user.id }) + .onConflictDoUpdate({ + target: [careAttribute.residentId, careAttribute.domain, careAttribute.key], + set: { value, updatedById: user.id }, + }) } revalidateResident(residentId) @@ -298,22 +326,20 @@ export async function createAppointment( return { success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS } } - const assigned = await prisma.careAssignment.findUnique({ - where: { residentId_role: { residentId, role: domain } }, - select: { staffId: true }, + const assigned = await db.query.careAssignment.findFirst({ + where: and(eq(careAssignment.residentId, residentId), eq(careAssignment.role, domain)), + columns: { staffId: true }, }) const staffId = assigned?.staffId || user.id - await prisma.appointment.create({ - data: { - residentId, - staffId, - domain, - title, - startsAt, - location: String(formData.get('location') || '').trim() || null, - notes: String(formData.get('notes') || '').trim() || null, - }, + await db.insert(appointmentTable).values({ + residentId, + staffId, + domain, + title, + startsAt, + location: String(formData.get('location') || '').trim() || null, + notes: String(formData.get('notes') || '').trim() || null, }) revalidateResident(residentId) @@ -330,9 +356,10 @@ export async function setAppointmentStatus( const status = parseStatus(formData.get('status')) if (!id || !status) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } - const appointment = await prisma.appointment.findUnique({ - where: { id }, - select: { residentId: true, domain: true, checkIn: { select: { id: true } } }, + const appointment = await db.query.appointment.findFirst({ + where: eq(appointmentTable.id, id), + columns: { residentId: true, domain: true }, + with: { checkIn: { columns: { id: true } } }, }) if (!appointment) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } if (!canWriteCareDomain(user, appointment.domain)) { @@ -348,35 +375,36 @@ export async function setAppointmentStatus( const rating = parseSatisfaction(formData.get('overallSatisfaction')) const concerns = String(formData.get('concerns') || '').trim() - await prisma.appointment.update({ where: { id }, data: { status } }) + await db.update(appointmentTable).set({ status }).where(eq(appointmentTable.id, id)) if (status === 'COMPLETED' && rating !== null && !appointment.checkIn) { // The check-in hangs off a placement, so someone with no active placement // can still have appointments — they just have nothing to attach a // reading to. Completing the appointment must not fail because of that. - const placement = await prisma.placement.findFirst({ - where: { residentId: appointment.residentId, status: 'ACTIVE' }, - select: { id: true, startDate: true }, + const placement = await db.query.placement.findFirst({ + where: and( + eq(placementTable.residentId, appointment.residentId), + eq(placementTable.status, 'ACTIVE'), + ), + columns: { id: true, startDate: true }, }) if (placement) { - await prisma.$transaction(async (tx) => { - await tx.satisfactionCheckIn.create({ - data: { - placementId: placement.id, - appointmentId: id, - checkInType: 'AD_HOC', - weekNumber: weeksBetween(placement.startDate), - overallSatisfaction: rating, - concerns: concerns || null, - collectedByUserId: user.id, - isAnonymous: false, - }, - }) - await tx.placement.update({ - where: { id: placement.id }, - data: { satisfactionRating: rating }, + await db.transaction(async (tx) => { + await tx.insert(satisfactionCheckIn).values({ + placementId: placement.id, + appointmentId: id, + checkInType: 'AD_HOC', + weekNumber: weeksBetween(placement.startDate), + overallSatisfaction: rating, + concerns: concerns || null, + collectedByUserId: user.id, + isAnonymous: false, }) + await tx + .update(placementTable) + .set({ satisfactionRating: rating }) + .where(eq(placementTable.id, placement.id)) }) await logAudit({ @@ -448,30 +476,32 @@ export async function requestAppointment( // One open request per seat. Without this a resident who taps twice, or who // is not sure the first one worked, fills a coach's queue with duplicates of // the same ask — and nothing in the UI told them the first had landed. - const existing = await prisma.appointment.findFirst({ - where: { residentId: auth.resident.id, domain, status: 'REQUESTED' }, - select: { id: true }, + const existing = await db.query.appointment.findFirst({ + where: and( + eq(appointmentTable.residentId, auth.resident.id), + eq(appointmentTable.domain, domain), + eq(appointmentTable.status, 'REQUESTED'), + ), + columns: { id: true }, }) if (existing) return { success: false, error: CARE_LABELS.requestDuplicate } // Whoever holds the seat, if anyone does. Null is a real state: on a // deployment where the care team is not assigned yet, the request is still // worth making and lands unclaimed rather than being refused. - const assigned = await prisma.careAssignment.findUnique({ - where: { residentId_role: { residentId: auth.resident.id, role: domain } }, - select: { staffId: true }, + const assigned = await db.query.careAssignment.findFirst({ + where: and(eq(careAssignment.residentId, auth.resident.id), eq(careAssignment.role, domain)), + columns: { staffId: true }, }) - await prisma.appointment.create({ - data: { - residentId: auth.resident.id, - staffId: assigned?.staffId ?? null, - domain, - title: CARE_LABELS.requestTitle, - startsAt, - status: 'REQUESTED', - residentNote: note || null, - }, + await db.insert(appointmentTable).values({ + residentId: auth.resident.id, + staffId: assigned?.staffId ?? null, + domain, + title: CARE_LABELS.requestTitle, + startsAt, + status: 'REQUESTED', + residentNote: note || null, }) revalidateResident(auth.resident.id) @@ -495,9 +525,9 @@ export async function respondToAppointmentRequest( const decision = String(formData.get('decision') || '') const note = String(formData.get('staffNote') || '').trim() - const appointment = await prisma.appointment.findUnique({ - where: { id }, - select: { residentId: true, domain: true, status: true }, + const appointment = await db.query.appointment.findFirst({ + where: eq(appointmentTable.id, id), + columns: { residentId: true, domain: true, status: true }, }) if (!appointment || appointment.status !== 'REQUESTED') { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } @@ -511,24 +541,24 @@ export async function respondToAppointmentRequest( // to do. The resident reads this sentence, so it is required. if (note.length < 3) return { success: false, error: CARE_LABELS.declineNeedsReason } - await prisma.appointment.update({ - where: { id }, - data: { status: 'CANCELLED', staffNote: note, staffId: user.id }, - }) + await db + .update(appointmentTable) + .set({ status: 'CANCELLED', staffNote: note, staffId: user.id }) + .where(eq(appointmentTable.id, id)) } else if (decision === 'ACCEPT') { const proposed = fromDatetimeLocalInput(String(formData.get('startsAt') || '')) - await prisma.appointment.update({ - where: { id }, - data: { + await db + .update(appointmentTable) + .set({ status: 'SCHEDULED', // The answering colleague takes it, which is also how an unclaimed // request gets an owner. staffId: user.id, ...(proposed ? { startsAt: proposed } : {}), staffNote: note || null, - }, - }) + }) + .where(eq(appointmentTable.id, id)) } else { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } @@ -564,9 +594,9 @@ export async function rescheduleAppointment( const note = String(formData.get('staffNote') || '').trim() if (!id || !startsAt) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } - const appointment = await prisma.appointment.findUnique({ - where: { id }, - select: { residentId: true, domain: true, status: true }, + const appointment = await db.query.appointment.findFirst({ + where: eq(appointmentTable.id, id), + columns: { residentId: true, domain: true, status: true }, }) if (!appointment) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } if (!canWriteCareDomain(user, appointment.domain)) { @@ -578,10 +608,10 @@ export async function rescheduleAppointment( return { success: false, error: CARE_LABELS.rescheduleClosed } } - await prisma.appointment.update({ - where: { id }, - data: { startsAt, staffNote: note || null }, - }) + await db + .update(appointmentTable) + .set({ startsAt, staffNote: note || null }) + .where(eq(appointmentTable.id, id)) await logAudit({ action: 'UPDATE', diff --git a/src/lib/actions/complaints.ts b/src/lib/actions/complaints.ts index f411b76f..597ba747 100644 --- a/src/lib/actions/complaints.ts +++ b/src/lib/actions/complaints.ts @@ -1,7 +1,8 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { db, complaint } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' import { logger } from '@/lib/logger' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -24,15 +25,19 @@ export async function respondToComplaint(formData: FormData): Promise { } try { - await prisma.complaint.update({ - where: { id: complaintId }, - data: { + const [updated] = await db + .update(complaint) + .set({ response, respondedAt: new Date(), respondedByUserId: user.id, status: 'ANSWERED', - }, - }) + }) + .where(eq(complaint.id, complaintId)) + .returning({ id: complaint.id }) + // Prisma's update threw on a missing row; keep that path so a bad id still + // surfaces as SAVE_ERROR instead of a silent no-op. + if (!updated) throw new Error('Complaint not found') } catch (error) { // The complaint body and the answer are both about a person's treatment. // Neither goes to the logger. diff --git a/src/lib/actions/config.ts b/src/lib/actions/config.ts index 2449391e..0d7f605e 100644 --- a/src/lib/actions/config.ts +++ b/src/lib/actions/config.ts @@ -1,7 +1,8 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { db, systemConfig } from '@/lib/db' +import { eq } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' export interface SystemConfigData { @@ -12,7 +13,9 @@ export interface SystemConfigData { } export async function getSystemConfig(): Promise { - const config = await prisma.systemConfig.findUnique({ where: { id: 'singleton' } }) + const config = await db.query.systemConfig.findFirst({ + where: eq(systemConfig.id, 'singleton'), + }) return { pilotBaselineIncidentsPerMonth: config?.pilotBaselineIncidentsPerMonth ?? null, pilotBaselineRelocationsPerMonth: config?.pilotBaselineRelocationsPerMonth ?? null, @@ -32,30 +35,21 @@ export async function saveSystemConfig(formData: FormData): Promise { const pilotStartRaw = formData.get('pilotStartDate') const pilotStartDate = pilotStartRaw ? new Date(pilotStartRaw as string) : null - await prisma.systemConfig.upsert({ - where: { id: 'singleton' }, - create: { - id: 'singleton', - pilotBaselineIncidentsPerMonth: parseFloat(formData.get('pilotBaselineIncidentsPerMonth')), - pilotBaselineRelocationsPerMonth: parseFloat( - formData.get('pilotBaselineRelocationsPerMonth'), - ), - pilotBaselineMediationHoursPerWeek: parseFloat( - formData.get('pilotBaselineMediationHoursPerWeek'), - ), - pilotStartDate, - }, - update: { - pilotBaselineIncidentsPerMonth: parseFloat(formData.get('pilotBaselineIncidentsPerMonth')), - pilotBaselineRelocationsPerMonth: parseFloat( - formData.get('pilotBaselineRelocationsPerMonth'), - ), - pilotBaselineMediationHoursPerWeek: parseFloat( - formData.get('pilotBaselineMediationHoursPerWeek'), - ), - pilotStartDate, - }, - }) + // Prisma's upsert had identical create and update payloads, so the insert + // values double as the conflict-update set. + const values = { + pilotBaselineIncidentsPerMonth: parseFloat(formData.get('pilotBaselineIncidentsPerMonth')), + pilotBaselineRelocationsPerMonth: parseFloat(formData.get('pilotBaselineRelocationsPerMonth')), + pilotBaselineMediationHoursPerWeek: parseFloat( + formData.get('pilotBaselineMediationHoursPerWeek'), + ), + pilotStartDate, + } + + await db + .insert(systemConfig) + .values({ id: 'singleton', ...values }) + .onConflictDoUpdate({ target: systemConfig.id, set: values }) revalidatePath('/settings') revalidatePath('/analytics') diff --git a/src/lib/actions/documents.ts b/src/lib/actions/documents.ts index 67fa483f..026e1032 100644 --- a/src/lib/actions/documents.ts +++ b/src/lib/actions/documents.ts @@ -1,7 +1,8 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { db, resident as residentTable, residentDocument, residentDocumentBlob } from '@/lib/db' +import { desc, eq } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' import { logAudit } from '@/lib/audit' import { logger } from '@/lib/logger' @@ -37,9 +38,9 @@ export async function listResidentDocuments( ): Promise { await requirePermission('documents:read') - const rows = await prisma.residentDocument.findMany({ - where: { residentId }, - select: { + const rows = await db.query.residentDocument.findMany({ + where: eq(residentDocument.residentId, residentId), + columns: { id: true, category: true, title: true, @@ -47,9 +48,9 @@ export async function listResidentDocuments( mimeType: true, sizeBytes: true, createdAt: true, - uploadedBy: { select: { name: true } }, }, - orderBy: { createdAt: 'desc' }, + with: { uploadedBy: { columns: { name: true } } }, + orderBy: [desc(residentDocument.createdAt)], }) return rows.map((row) => ({ @@ -89,9 +90,9 @@ export async function uploadResidentDocument( return { success: false, error: DOCUMENT_LABELS.wrongType } } - const resident = await prisma.resident.findUnique({ - where: { id: residentId }, - select: { id: true }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + columns: { id: true }, }) if (!resident) return { success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND } @@ -100,9 +101,10 @@ export async function uploadResidentDocument( try { const bytes = Buffer.from(await file.arrayBuffer()) - const created = await prisma.$transaction(async (tx) => { - const document = await tx.residentDocument.create({ - data: { + const created = await db.transaction(async (tx) => { + const [document] = await tx + .insert(residentDocument) + .values({ residentId, category, title: title.slice(0, 200), @@ -110,11 +112,9 @@ export async function uploadResidentDocument( mimeType: file.type, sizeBytes: file.size, uploadedByUserId: user.id, - }, - }) - await tx.residentDocumentBlob.create({ - data: { documentId: document.id, data: bytes }, - }) + }) + .returning() + await tx.insert(residentDocumentBlob).values({ documentId: document.id, data: bytes }) return document }) @@ -148,16 +148,16 @@ export async function deleteResidentDocument( const user = await requirePermission('documents:write') const id = String(formData.get('id') || '') - const document = await prisma.residentDocument.findUnique({ - where: { id }, - select: { id: true, residentId: true, fileName: true, category: true }, + const document = await db.query.residentDocument.findFirst({ + where: eq(residentDocument.id, id), + columns: { id: true, residentId: true, fileName: true, category: true }, }) if (!document) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } try { // The blob cascades. Deleting the metadata row and orphaning bytes would // leave a file nobody can see and nobody can remove. - await prisma.residentDocument.delete({ where: { id } }) + await db.delete(residentDocument).where(eq(residentDocument.id, id)) await logAudit({ action: 'DELETE', diff --git a/src/lib/actions/events.ts b/src/lib/actions/events.ts index 5a5e11c6..d35b01b5 100644 --- a/src/lib/actions/events.ts +++ b/src/lib/actions/events.ts @@ -1,14 +1,15 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { db, eventRsvp, houseEvent } from '@/lib/db' +import type { EventRsvpStatus, HouseEventCategory, HouseEventStatus } from '@/lib/db' +import { and, asc, desc, eq, ne } from 'drizzle-orm' import { getCurrentUser } from '@/lib/auth' import { getPortalAuth } from '@/lib/portal-auth' import { hasPermission } from '@/lib/auth/role-policy' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' import { RESIDENT_NAME_SELECT, residentName } from '@/lib/utils/resident-name' import { fromDatetimeLocalInput } from '@/lib/utils/local-time' -import type { EventRsvpStatus, HouseEventCategory, HouseEventStatus } from '@prisma/client' const CATEGORIES: HouseEventCategory[] = ['HOUSE_MEETING', 'SOCIAL', 'CULTURE', 'SUPPORT'] const RSVP_STATUSES: EventRsvpStatus[] = ['GOING', 'MAYBE', 'DECLINED'] @@ -99,15 +100,18 @@ export async function listUnitEvents(now: Date = new Date()): Promise<{ const auth = await getPortalAuth() if (!auth) return null - const rows = await prisma.houseEvent.findMany({ - where: { housingUnitId: auth.placement.housingUnitId, status: { not: 'CANCELLED' } }, - include: { - housingUnit: { select: { id: true, code: true } }, - createdByStaff: { select: { name: true } }, - createdByResident: { select: RESIDENT_NAME_SELECT }, - rsvps: { include: { resident: { select: RESIDENT_NAME_SELECT } } }, + const rows = await db.query.houseEvent.findMany({ + where: and( + eq(houseEvent.housingUnitId, auth.placement.housingUnitId), + ne(houseEvent.status, 'CANCELLED'), + ), + with: { + housingUnit: { columns: { id: true, code: true } }, + createdByStaff: { columns: { name: true } }, + createdByResident: { columns: RESIDENT_NAME_SELECT }, + rsvps: { with: { resident: { columns: RESIDENT_NAME_SELECT } } }, }, - orderBy: { startsAt: 'asc' }, + orderBy: [asc(houseEvent.startsAt)], }) const mapped = rows.map(mapEvent) @@ -118,14 +122,14 @@ export async function listUnitEvents(now: Date = new Date()): Promise<{ } export async function listStaffEvents(): Promise { - const rows = await prisma.houseEvent.findMany({ - include: { - housingUnit: { select: { id: true, code: true } }, - createdByStaff: { select: { name: true } }, - createdByResident: { select: RESIDENT_NAME_SELECT }, - rsvps: { include: { resident: { select: RESIDENT_NAME_SELECT } } }, + const rows = await db.query.houseEvent.findMany({ + with: { + housingUnit: { columns: { id: true, code: true } }, + createdByStaff: { columns: { name: true } }, + createdByResident: { columns: RESIDENT_NAME_SELECT }, + rsvps: { with: { resident: { columns: RESIDENT_NAME_SELECT } } }, }, - orderBy: { startsAt: 'desc' }, + orderBy: [desc(houseEvent.startsAt)], }) return rows.map(mapEvent) } @@ -145,16 +149,14 @@ export async function createEventAsResident( return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } - await prisma.houseEvent.create({ - data: { - housingUnitId: auth.placement.housingUnitId, - createdByResidentId: auth.resident.id, - title, - description, - location, - startsAt, - category, - }, + await db.insert(houseEvent).values({ + housingUnitId: auth.placement.housingUnitId, + createdByResidentId: auth.resident.id, + title, + description, + location, + startsAt, + category, }) revalidateEvents() @@ -180,16 +182,14 @@ export async function createEventAsStaff( return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } - await prisma.houseEvent.create({ - data: { - housingUnitId, - createdByStaffId: user.id, - title, - description, - location, - startsAt, - category, - }, + await db.insert(houseEvent).values({ + housingUnitId, + createdByStaffId: user.id, + title, + description, + location, + startsAt, + category, }) revalidateEvents() @@ -210,9 +210,9 @@ export async function rsvpToEvent( // check any resident could answer any unit's event by id — and since the // attendee list renders NAMES, that puts a stranger into another household's // "wer kommt" list, which is a privacy leak wearing the costume of an RSVP. - const event = await prisma.houseEvent.findUnique({ - where: { id: eventId }, - select: { housingUnitId: true, status: true }, + const event = await db.query.houseEvent.findFirst({ + where: eq(houseEvent.id, eventId), + columns: { housingUnitId: true, status: true }, }) if ( !event || @@ -222,11 +222,13 @@ export async function rsvpToEvent( return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } - await prisma.eventRsvp.upsert({ - where: { eventId_residentId: { eventId, residentId: auth.resident.id } }, - create: { eventId, residentId: auth.resident.id, status }, - update: { status }, - }) + await db + .insert(eventRsvp) + .values({ eventId, residentId: auth.resident.id, status }) + .onConflictDoUpdate({ + target: [eventRsvp.eventId, eventRsvp.residentId], + set: { status }, + }) revalidateEvents() return { success: true } @@ -236,22 +238,22 @@ export async function cancelEvent( formData: FormData, ): Promise<{ success: boolean; error?: string }> { const id = String(formData.get('id') || '') - const event = await prisma.houseEvent.findUnique({ - where: { id }, - select: { createdByResidentId: true, createdByStaffId: true }, + const event = await db.query.houseEvent.findFirst({ + where: eq(houseEvent.id, id), + columns: { createdByResidentId: true, createdByStaffId: true }, }) if (!event) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } const staffUser = await getCurrentUser() if (staffUser && hasPermission(staffUser, 'events:write')) { - await prisma.houseEvent.update({ where: { id }, data: { status: 'CANCELLED' } }) + await db.update(houseEvent).set({ status: 'CANCELLED' }).where(eq(houseEvent.id, id)) revalidateEvents() return { success: true } } const auth = await getPortalAuth() if (auth && event.createdByResidentId === auth.resident.id) { - await prisma.houseEvent.update({ where: { id }, data: { status: 'CANCELLED' } }) + await db.update(houseEvent).set({ status: 'CANCELLED' }).where(eq(houseEvent.id, id)) revalidateEvents() return { success: true } } diff --git a/src/lib/actions/governance.ts b/src/lib/actions/governance.ts index c387cd7c..2f5c334a 100644 --- a/src/lib/actions/governance.ts +++ b/src/lib/actions/governance.ts @@ -9,7 +9,16 @@ */ import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { + db, + agreementParty, + conflictAgreement, + houseRule, + incident, + incidentFollowUp, + proposal, +} from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { logAudit } from '@/lib/audit' import { logger } from '@/lib/logger' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -48,7 +57,7 @@ export async function syncOrgRuleCatalog(): Promise< > { const user = await requireStaffAuth() try { - const result = await syncOrgRules(prisma) + const result = await syncOrgRules(db) await logAudit({ action: 'UPDATE', @@ -81,9 +90,10 @@ export async function createOrgRule(input: OrgRuleInput): Promise { const user = await requireStaffAuth() try { - await prisma.houseRule.update({ - where: { id: ruleId }, - data: { status: 'ARCHIVED', effectiveUntil: new Date() }, - }) + const [archived] = await db + .update(houseRule) + .set({ status: 'ARCHIVED', effectiveUntil: new Date() }) + .where(eq(houseRule.id, ruleId)) + .returning({ id: houseRule.id }) + // Prisma threw on a missing rule; keep that outcome for a bad id. + if (!archived) return { success: false, error: ERROR_MESSAGES.ARCHIVE_ERROR } await logAudit({ action: 'ARCHIVE', entity: 'HOUSE_RULE', entityId: ruleId, userId: user.id }) @@ -190,15 +205,18 @@ export async function createUnitRuleAsStaff( } try { - const parent = await prisma.houseRule.findUnique({ where: { id: parsed.data.parentRuleId } }) + const parent = await db.query.houseRule.findFirst({ + where: eq(houseRule.id, parsed.data.parentRuleId), + }) if (!parent) return { success: false, error: ERROR_MESSAGES.RULE_NOT_FOUND } // Same gate as a resident proposal: staff cannot override an AOZ rule either. const check = checkUnitLegislation(parent) if (!check.allowed) return { success: false, error: check.reason } - const rule = await prisma.houseRule.create({ - data: { + const [rule] = await db + .insert(houseRule) + .values({ scope: 'UNIT', housingUnitId: parsed.data.housingUnitId, parentRuleId: parsed.data.parentRuleId, @@ -209,8 +227,8 @@ export async function createUnitRuleAsStaff( status: 'ACTIVE', version: 1, createdByStaff: user.name, - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', @@ -275,18 +293,24 @@ export async function confirmProposal(input: StaffConfirmProposalInput): Promise try { // Atomic guard — two staff members must not confirm and veto the same row. - const updated = await prisma.proposal.updateMany({ - where: { id: parsed.data.proposalId, status: 'NEEDS_STAFF_CONFIRMATION' }, - data: { + const updated = await db + .update(proposal) + .set({ status: parsed.data.decision === 'CONFIRMED' ? 'ACCEPTED' : 'VETOED', staffDecision: parsed.data.decision, staffNotes: parsed.data.staffNotes, staffUserId: user.id, staffDecidedAt: new Date(), - }, - }) - - if (updated.count === 0) { + }) + .where( + and( + eq(proposal.id, parsed.data.proposalId), + eq(proposal.status, 'NEEDS_STAFF_CONFIRMATION'), + ), + ) + .returning({ id: proposal.id }) + + if (updated.length === 0) { return { success: false, error: ERROR_MESSAGES.PROPOSAL_ALREADY_DECIDED } } @@ -327,25 +351,26 @@ export async function advanceResolutionStage(input: AdvanceStageInput): Promise< } try { - await prisma.incident.update({ - where: { id: parsed.data.incidentId }, - data: { + const [moved] = await db + .update(incident) + .set({ resolutionStage: parsed.data.stage, stageEnteredAt: new Date(), ...(parsed.data.stage === 'CLOSED' ? { resolvedAt: new Date() } : {}), - }, - }) + }) + .where(eq(incident.id, parsed.data.incidentId)) + .returning({ id: incident.id }) + // Prisma threw on a missing incident; keep that outcome for a bad id. + if (!moved) return { success: false, error: ERROR_MESSAGES.RESOLUTION_STAGE_ERROR } // The stage change itself is the follow-up record — the incident history // must show why it moved, not just that it did. if (parsed.data.note) { - await prisma.incidentFollowUp.create({ - data: { - incidentId: parsed.data.incidentId, - action: `Schritt gewechselt: ${parsed.data.stage}`, - notes: parsed.data.note, - staffName: user.name, - }, + await db.insert(incidentFollowUp).values({ + incidentId: parsed.data.incidentId, + action: `Schritt gewechselt: ${parsed.data.stage}`, + notes: parsed.data.note, + staffName: user.name, }) } @@ -380,17 +405,27 @@ export async function createAgreement( } try { - const agreement = await prisma.conflictAgreement.create({ - data: { - incidentId: parsed.data.incidentId, - terms: parsed.data.terms, - reviewDate: parsed.data.reviewDate, - mediatorName: parsed.data.mediatorName ?? user.name, - status: 'PROPOSED', - parties: { - create: parsed.data.residentIds.map((residentId) => ({ residentId })), - }, - }, + // Agreement and its parties land together or not at all — the nested + // create Prisma did in one call becomes two inserts in one transaction. + const agreement = await db.transaction(async (tx) => { + const [created] = await tx + .insert(conflictAgreement) + .values({ + incidentId: parsed.data.incidentId, + terms: parsed.data.terms, + reviewDate: parsed.data.reviewDate, + mediatorName: parsed.data.mediatorName ?? user.name, + status: 'PROPOSED', + }) + .returning() + if (parsed.data.residentIds.length > 0) { + await tx + .insert(agreementParty) + .values( + parsed.data.residentIds.map((residentId) => ({ agreementId: created.id, residentId })), + ) + } + return created }) await logAudit({ @@ -426,15 +461,17 @@ export async function reviewAgreement(input: ReviewAgreementInput): Promise { const user = await requirePermission('housing:write') @@ -17,12 +26,14 @@ export async function createHousingUnit(formData: FormData): Promise { let unit try { - unit = await prisma.housingUnit.create({ - data: { + const [created] = await db + .insert(housingUnit) + .values({ ...data, status: DEFAULT_STATUSES.housing, - }, - }) + }) + .returning() + unit = created await logAudit({ action: 'CREATE', @@ -32,7 +43,7 @@ export async function createHousingUnit(formData: FormData): Promise { changes: { code: data.code, address: data.address }, }) } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { throw new Error(ERROR_MESSAGES.UNIT_CODE_EXISTS) } logger.errorWithCause('Failed to create housing unit', error, { code: data.code }) @@ -50,10 +61,13 @@ export async function updateHousingUnit(formData: FormData): Promise { const { id, ...updateData } = data try { - await prisma.housingUnit.update({ - where: { id }, - data: updateData, - }) + const [updated] = await db + .update(housingUnit) + .set(updateData) + .where(eq(housingUnit.id, id)) + .returning({ id: housingUnit.id }) + // Prisma threw when the unit was missing — keep that error path + if (!updated) throw new Error(ERROR_MESSAGES.UNIT_UPDATE_ERROR) await logAudit({ action: 'UPDATE', @@ -77,11 +91,11 @@ export async function archiveHousingUnit( ): Promise<{ success: boolean; error?: string }> { const user = await requirePermission('housing:write') try { - const unit = await prisma.housingUnit.findUnique({ - where: { id: housingUnitId }, - include: { - placements: { where: { status: 'ACTIVE' }, select: { id: true } }, - spots: { where: { status: 'OCCUPIED' }, select: { id: true } }, + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, housingUnitId), + with: { + placements: { where: eq(placement.status, 'ACTIVE'), columns: { id: true } }, + spots: { where: eq(placementSpot.status, 'OCCUPIED'), columns: { id: true } }, }, }) @@ -90,10 +104,7 @@ export async function archiveHousingUnit( return { success: false, error: ERROR_MESSAGES.UNIT_ARCHIVE_BLOCKED } } - await prisma.housingUnit.update({ - where: { id: housingUnitId }, - data: { status: 'CLOSED' }, - }) + await db.update(housingUnit).set({ status: 'CLOSED' }).where(eq(housingUnit.id, housingUnitId)) await logAudit({ action: 'ARCHIVE', @@ -117,13 +128,13 @@ export async function restoreHousingUnit( ): Promise<{ success: boolean; error?: string }> { const user = await requirePermission('housing:write') try { - const unit = await prisma.housingUnit.findUnique({ where: { id: housingUnitId } }) + const unit = await db.query.housingUnit.findFirst({ where: eq(housingUnit.id, housingUnitId) }) if (!unit) return { success: false, error: ERROR_MESSAGES.UNIT_NOT_FOUND } - await prisma.housingUnit.update({ - where: { id: housingUnitId }, - data: { status: 'AVAILABLE' }, - }) + await db + .update(housingUnit) + .set({ status: 'AVAILABLE' }) + .where(eq(housingUnit.id, housingUnitId)) await logAudit({ action: 'RESTORE', @@ -165,7 +176,7 @@ export async function hardDeleteHousingUnitProtected( } } - const unit = await prisma.housingUnit.findUnique({ where: { id: housingUnitId } }) + const unit = await db.query.housingUnit.findFirst({ where: eq(housingUnit.id, housingUnitId) }) if (!unit) return { success: false, error: ERROR_MESSAGES.UNIT_NOT_FOUND } if (!isTestOrDemoCode(unit.code)) { @@ -173,11 +184,11 @@ export async function hardDeleteHousingUnitProtected( } const [placements, incidents, maintenanceRequests, spots, tasks] = await Promise.all([ - prisma.placement.count({ where: { housingUnitId } }), - prisma.incident.count({ where: { housingUnitId } }), - prisma.maintenanceRequest.count({ where: { housingUnitId } }), - prisma.placementSpot.count({ where: { housingUnitId } }), - prisma.householdTask.count({ where: { housingUnitId } }), + db.$count(placement, eq(placement.housingUnitId, housingUnitId)), + db.$count(incident, eq(incident.housingUnitId, housingUnitId)), + db.$count(maintenanceRequest, eq(maintenanceRequest.housingUnitId, housingUnitId)), + db.$count(placementSpot, eq(placementSpot.housingUnitId, housingUnitId)), + db.$count(householdTask, eq(householdTask.housingUnitId, housingUnitId)), ]) if (placements + incidents + maintenanceRequests + spots + tasks > 0) { @@ -194,7 +205,7 @@ export async function hardDeleteHousingUnitProtected( } } - await prisma.housingUnit.delete({ where: { id: housingUnitId } }) + await db.delete(housingUnit).where(eq(housingUnit.id, housingUnitId)) await logAudit({ action: 'DELETE', diff --git a/src/lib/actions/incidents.ts b/src/lib/actions/incidents.ts index 423a9313..cb02b7f6 100644 --- a/src/lib/actions/incidents.ts +++ b/src/lib/actions/incidents.ts @@ -2,7 +2,8 @@ import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, incident, incidentFollowUp, incidentInvolvement } from '@/lib/db' +import { and, asc, desc, eq, gte, inArray, isNull, lt, lte, or } from 'drizzle-orm' import { z } from 'zod' import { validateFormData, @@ -29,8 +30,9 @@ export async function createIncident(formData: FormData): Promise { let incidentId: string try { - const incident = await prisma.incident.create({ - data: { + const [created] = await db + .insert(incident) + .values({ housingUnitId: data.housingUnitId, reportedById: data.reportedById || undefined, subjectId: data.subjectId || undefined, @@ -49,13 +51,13 @@ export async function createIncident(formData: FormData): Promise { type: data.type, }).stage, stageEnteredAt: new Date(), - }, - }) + }) + .returning() await logAudit({ action: 'CREATE', entity: 'INCIDENT', - entityId: incident.id, + entityId: created.id, userId: user.id, changes: { category: data.category, @@ -64,7 +66,7 @@ export async function createIncident(formData: FormData): Promise { housingUnitId: data.housingUnitId, }, }) - incidentId = incident.id + incidentId = created.id } catch (error) { logger.errorWithCause('Failed to create incident', error, { housingUnitId: data.housingUnitId }) throw new Error(ERROR_MESSAGES.INCIDENT_CREATE_ERROR) @@ -78,13 +80,16 @@ export async function resolveIncident(formData: FormData): Promise { const { incidentId, resolution } = validateFormData(ResolveIncidentSchema, formData) try { - await prisma.incident.update({ - where: { id: incidentId }, - data: { + const [updated] = await db + .update(incident) + .set({ resolvedAt: new Date(), resolution, - }, - }) + }) + .where(eq(incident.id, incidentId)) + .returning({ id: incident.id }) + // Prisma threw when the incident was missing — keep that error path + if (!updated) throw new Error(ERROR_MESSAGES.INCIDENT_RESOLVE_ERROR) await logAudit({ action: 'RESOLVE', @@ -107,10 +112,13 @@ export async function updateMediationTime(formData: FormData): Promise { const { incidentId, mediationMinutes } = validateFormData(UpdateMediationTimeSchema, formData) try { - await prisma.incident.update({ - where: { id: incidentId }, - data: { mediationMinutes }, - }) + const [updated] = await db + .update(incident) + .set({ mediationMinutes }) + .where(eq(incident.id, incidentId)) + .returning({ id: incident.id }) + // Prisma threw when the incident was missing — keep that error path + if (!updated) throw new Error(ERROR_MESSAGES.INCIDENT_UPDATE_MEDIATION_ERROR) await logAudit({ action: 'UPDATE', @@ -138,27 +146,25 @@ export interface ResidentIncidentStats { export async function getResidentIncidentStats(residentId: string): Promise { await requirePermission('incidents:read') const [reported, asSubject, involved] = await Promise.all([ - prisma.incident.count({ - where: { reportedById: residentId }, - }), - prisma.incident.count({ - where: { subjectId: residentId }, - }), - prisma.incidentInvolvement.count({ - where: { residentId }, - }), + db.$count(incident, eq(incident.reportedById, residentId)), + db.$count(incident, eq(incident.subjectId, residentId)), + db.$count(incidentInvolvement, eq(incidentInvolvement.residentId, residentId)), ]) // Count unique incidents (reporter, subject, or involved) - const uniqueIncidents = await prisma.incident.findMany({ - where: { - OR: [ - { reportedById: residentId }, - { subjectId: residentId }, - { involvedResidents: { some: { residentId } } }, - ], - }, - select: { id: true }, + const uniqueIncidents = await db.query.incident.findMany({ + where: or( + eq(incident.reportedById, residentId), + eq(incident.subjectId, residentId), + inArray( + incident.id, + db + .select({ id: incidentInvolvement.incidentId }) + .from(incidentInvolvement) + .where(eq(incidentInvolvement.residentId, residentId)), + ), + ), + columns: { id: true }, }) return { @@ -171,19 +177,19 @@ export async function getResidentIncidentStats(residentId: string): Promise { try { // Create the follow-up record - const followUp = await prisma.incidentFollowUp.create({ - data: { + const [followUp] = await db + .insert(incidentFollowUp) + .values({ incidentId: data.incidentId, action: data.action, notes: data.notes, outcome: data.outcome, staffName: data.staffName, scheduledNextDate: data.scheduledNextDate, - }, - }) + }) + .returning() // Update the incident with next follow-up date and priority - const updateData: Record = {} + const updateData: Partial = {} if (data.scheduledNextDate) { updateData.nextFollowUpDate = data.scheduledNextDate } @@ -246,10 +253,7 @@ export async function addFollowUp(formData: FormData): Promise { } if (Object.keys(updateData).length > 0) { - await prisma.incident.update({ - where: { id: data.incidentId }, - data: updateData, - }) + await db.update(incident).set(updateData).where(eq(incident.id, data.incidentId)) } await logAudit({ @@ -270,20 +274,22 @@ export async function addFollowUp(formData: FormData): Promise { export async function getIncidentWithFollowUps(incidentId: string) { await requirePermission('incidents:read') - return prisma.incident.findUnique({ - where: { id: incidentId }, - include: { - housingUnit: true, - reportedBy: true, - subject: true, - involvedResidents: { - include: { resident: true }, - }, - followUps: { - orderBy: { createdAt: 'desc' }, + return ( + (await db.query.incident.findFirst({ + where: eq(incident.id, incidentId), + with: { + housingUnit: true, + reportedBy: true, + subject: true, + involvedResidents: { + with: { resident: true }, + }, + followUps: { + orderBy: [desc(incidentFollowUp.createdAt)], + }, }, - }, - }) + })) ?? null + ) } export async function getIncidentsNeedingFollowUp() { @@ -295,49 +301,60 @@ export async function getIncidentsNeedingFollowUp() { threeDays.setDate(threeDays.getDate() + 3) // Get overdue incidents (follow-up date passed but not resolved) - const overdue = await prisma.incident.findMany({ - where: { - resolvedAt: null, - nextFollowUpDate: { lt: now }, - }, - include: { + const overdue = await db.query.incident.findMany({ + where: and(isNull(incident.resolvedAt), lt(incident.nextFollowUpDate, now)), + with: { housingUnit: true, subject: true, - _count: { select: { followUps: true } }, + followUps: { columns: { id: true } }, }, - orderBy: { nextFollowUpDate: 'asc' }, + orderBy: [asc(incident.nextFollowUpDate)], }) // Get incidents due today/tomorrow - const dueSoon = await prisma.incident.findMany({ - where: { - resolvedAt: null, - nextFollowUpDate: { gte: now, lte: tomorrow }, - }, - include: { + const dueSoon = await db.query.incident.findMany({ + where: and( + isNull(incident.resolvedAt), + gte(incident.nextFollowUpDate, now), + lte(incident.nextFollowUpDate, tomorrow), + ), + with: { housingUnit: true, subject: true, - _count: { select: { followUps: true } }, + followUps: { columns: { id: true } }, }, - orderBy: { nextFollowUpDate: 'asc' }, + orderBy: [asc(incident.nextFollowUpDate)], }) // Get incidents with urgent priority regardless of date - const urgent = await prisma.incident.findMany({ - where: { - resolvedAt: null, - followUpPriority: { in: ['URGENT', 'HIGH'] }, - nextFollowUpDate: { gte: tomorrow }, // Not already in dueSoon - }, - include: { + const urgent = await db.query.incident.findMany({ + where: and( + isNull(incident.resolvedAt), + inArray(incident.followUpPriority, ['URGENT', 'HIGH']), + gte(incident.nextFollowUpDate, tomorrow), // Not already in dueSoon + ), + with: { housingUnit: true, subject: true, - _count: { select: { followUps: true } }, + followUps: { columns: { id: true } }, }, - orderBy: { followUpPriority: 'asc' }, + orderBy: [asc(incident.followUpPriority)], }) - return { overdue, dueSoon, urgent } + // Rebuild Prisma's `_count: { followUps }` shape — the query API has no + // count-include, so we fetched the follow-up ids and count them in memory. + const withFollowUpCount = ({ + followUps, + ...rest + }: (typeof overdue)[number]): Omit<(typeof overdue)[number], 'followUps'> & { + _count: { followUps: number } + } => ({ ...rest, _count: { followUps: followUps.length } }) + + return { + overdue: overdue.map(withFollowUpCount), + dueSoon: dueSoon.map(withFollowUpCount), + urgent: urgent.map(withFollowUpCount), + } } export async function clearFollowUpReminder(formData: FormData): Promise { @@ -345,13 +362,16 @@ export async function clearFollowUpReminder(formData: FormData): Promise { const { incidentId } = validateFormData(ClearFollowUpSchema, formData) try { - await prisma.incident.update({ - where: { id: incidentId }, - data: { + const [updated] = await db + .update(incident) + .set({ nextFollowUpDate: null, followUpPriority: null, - }, - }) + }) + .where(eq(incident.id, incidentId)) + .returning({ id: incident.id }) + // Prisma threw when the incident was missing — keep that error path + if (!updated) throw new Error(ERROR_MESSAGES.REMINDER_DELETE_ERROR) await logAudit({ action: 'UPDATE', diff --git a/src/lib/actions/learning.ts b/src/lib/actions/learning.ts index 6ca1458d..18b8ecc9 100644 --- a/src/lib/actions/learning.ts +++ b/src/lib/actions/learning.ts @@ -1,7 +1,8 @@ 'use server' import { revalidatePath } from 'next/cache' -import { prisma } from '@/lib/db' +import { db, learningRecord, resident, careAssignment, placement, escapeLike } from '@/lib/db' +import { and, asc, count, desc, eq, ilike, inArray, notInArray, or, sql } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' import { getResidentCookie } from '@/lib/portal-auth' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -13,7 +14,7 @@ import { LEARNING_KINDS, LEARNING_STATUSES, } from '@/lib/config/learning' -import type { LearningKind, LearningStatus, ResidentOrStaff } from '@prisma/client' +import type { LearningKind, LearningStatus, ResidentOrStaff } from '@/lib/db' const INVALID_RECORD_MESSAGE = 'Art und Bezeichnung sind erforderlich' @@ -83,9 +84,9 @@ export async function createLearningRecordForResident(formData: FormData): Promi if (!residentId) throw new Error(ERROR_MESSAGES.RESIDENT_NOT_FOUND) const data = parseRecord(formData) - await prisma.learningRecord.create({ - data: { ...data, residentId, recordedBy: 'STAFF' as ResidentOrStaff }, - }) + await db + .insert(learningRecord) + .values({ ...data, residentId, recordedBy: 'STAFF' as ResidentOrStaff }) revalidatePath(`/residents/${residentId}`) revalidatePath('/learning') @@ -98,17 +99,17 @@ export async function createOwnLearningRecord( const code = await getResidentCookie() if (!code) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } - const resident = await prisma.resident.findUnique({ - where: { code }, - select: { id: true }, + const residentRow = await db.query.resident.findFirst({ + where: eq(resident.code, code), + columns: { id: true }, }) - if (!resident) return { success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND } + if (!residentRow) return { success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND } try { const data = parseRecord(formData) - await prisma.learningRecord.create({ - data: { ...data, residentId: resident.id, recordedBy: 'RESIDENT' }, - }) + await db + .insert(learningRecord) + .values({ ...data, residentId: residentRow.id, recordedBy: 'RESIDENT' }) } catch (error) { if (error instanceof Error && error.message === INVALID_RECORD_MESSAGE) { return { success: false, error: ERROR_MESSAGES.INVALID_INPUT_DATA } @@ -123,33 +124,39 @@ export async function createOwnLearningRecord( return { success: true } } +// Residents with no German language test on file (Prisma's `learningRecords: { none: … }`) +function missingGermanTestFilter() { + return notInArray( + resident.id, + db + .select({ id: learningRecord.residentId }) + .from(learningRecord) + .where(and(eq(learningRecord.kind, 'LANGUAGE_TEST'), eq(learningRecord.languageCode, 'DE'))), + ) +} + export async function listLearningQueue(kind?: LearningKind) { await requirePermission('learning:read') const [records, missingGerman] = await Promise.all([ - prisma.learningRecord.findMany({ - where: { - status: { in: ['PLANNED', 'IN_PROGRESS'] }, - ...(kind ? { kind } : {}), + db.query.learningRecord.findMany({ + where: and( + inArray(learningRecord.status, ['PLANNED', 'IN_PROGRESS']), + ...(kind ? [eq(learningRecord.kind, kind)] : []), + ), + with: { + resident: { columns: { id: true, code: true, displayName: true, languages: true } }, }, - include: { - resident: { select: { id: true, code: true, displayName: true, languages: true } }, - }, - orderBy: { updatedAt: 'desc' }, - take: 50, + orderBy: [desc(learningRecord.updatedAt)], + limit: 50, }), kind ? Promise.resolve([]) - : prisma.resident.findMany({ - where: { - status: { in: ['ACTIVE', 'PLACED'] }, - learningRecords: { - none: { kind: 'LANGUAGE_TEST', languageCode: 'DE' }, - }, - }, - select: { id: true, code: true, displayName: true, languages: true }, - orderBy: { code: 'asc' }, - take: 40, + : db.query.resident.findMany({ + where: and(inArray(resident.status, ['ACTIVE', 'PLACED']), missingGermanTestFilter()), + columns: { id: true, code: true, displayName: true, languages: true }, + orderBy: [asc(resident.code)], + limit: 40, }), ]) @@ -169,97 +176,115 @@ export async function listLearningBoard(filters: LearningBoardFilters) { const user = await requirePermission('learning:read') const query = filters.query?.trim() || '' const kinds = boardKinds(filters.board) + const pattern = `%${escapeLike(query)}%` + + // Residents assigned to me (Prisma's `careAssignments: { some: { staffId } }`) + const myResidentIds = db + .select({ id: careAssignment.residentId }) + .from(careAssignment) + .where(eq(careAssignment.staffId, user.id)) - const residentWhere = filters.mineOnly - ? { careAssignments: { some: { staffId: user.id } } } - : undefined + const residentWhere = filters.mineOnly ? inArray(resident.id, myResidentIds) : undefined - const recordWhere = { - kind: { in: [...kinds] as LearningKind[] }, - ...(filters.status && filters.status !== 'ALL' ? { status: filters.status } : {}), + const recordWhere = and( + kinds.length ? inArray(learningRecord.kind, [...kinds] as LearningKind[]) : sql`false`, + ...(filters.status && filters.status !== 'ALL' + ? [eq(learningRecord.status, filters.status)] + : []), ...(filters.recordedBy && filters.recordedBy !== 'ALL' - ? { recordedBy: filters.recordedBy } - : {}), - ...(filters.category && filters.category !== 'ALL' ? { category: filters.category } : {}), + ? [eq(learningRecord.recordedBy, filters.recordedBy)] + : []), + ...(filters.category && filters.category !== 'ALL' + ? [eq(learningRecord.category, filters.category)] + : []), ...(query - ? { - OR: [ - { title: { contains: query, mode: 'insensitive' as const } }, - { provider: { contains: query, mode: 'insensitive' as const } }, - { notes: { contains: query, mode: 'insensitive' as const } }, - { resident: { code: { contains: query, mode: 'insensitive' as const } } }, - { resident: { displayName: { contains: query, mode: 'insensitive' as const } } }, - ], - } - : {}), - ...(residentWhere ? { resident: residentWhere } : {}), - } + ? [ + or( + ilike(learningRecord.title, pattern), + ilike(learningRecord.provider, pattern), + ilike(learningRecord.notes, pattern), + inArray( + learningRecord.residentId, + db + .select({ id: resident.id }) + .from(resident) + .where(or(ilike(resident.code, pattern), ilike(resident.displayName, pattern))), + ), + ), + ] + : []), + ...(filters.mineOnly ? [inArray(learningRecord.residentId, myResidentIds)] : []), + ) const [records, missingGerman, total, statusGroups, sourceGroups] = await Promise.all([ - prisma.learningRecord.findMany({ + db.query.learningRecord.findMany({ where: recordWhere, - include: { + with: { resident: { - select: { + columns: { id: true, code: true, displayName: true, supportLevel: true, + }, + with: { placements: { - where: { status: 'ACTIVE' }, - select: { housingUnit: { select: { code: true } } }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + columns: {}, + with: { housingUnit: { columns: { code: true } } }, + limit: 1, }, }, }, }, - orderBy: [{ status: 'asc' }, { updatedAt: 'desc' }], - take: 200, + orderBy: [asc(learningRecord.status), desc(learningRecord.updatedAt)], + limit: 200, }), filters.board === 'volunteering' ? Promise.resolve([]) - : prisma.resident.findMany({ - where: { - status: { in: ['ACTIVE', 'PLACED'] }, - learningRecords: { - none: { kind: 'LANGUAGE_TEST', languageCode: 'DE' }, - }, - ...(residentWhere || {}), - }, - select: { + : db.query.resident.findMany({ + where: and( + inArray(resident.status, ['ACTIVE', 'PLACED']), + missingGermanTestFilter(), + ...(residentWhere ? [residentWhere] : []), + ), + columns: { id: true, code: true, displayName: true, supportLevel: true, + }, + with: { placements: { - where: { status: 'ACTIVE' }, - select: { housingUnit: { select: { code: true } } }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + columns: {}, + with: { housingUnit: { columns: { code: true } } }, + limit: 1, }, }, - orderBy: { code: 'asc' }, - take: 40, + orderBy: [asc(resident.code)], + limit: 40, }), - prisma.learningRecord.count({ where: recordWhere }), - prisma.learningRecord.groupBy({ - by: ['status'], - where: recordWhere, - _count: { _all: true }, - }), - prisma.learningRecord.groupBy({ - by: ['recordedBy'], - where: recordWhere, - _count: { _all: true }, - }), + db.$count(learningRecord, recordWhere), + db + .select({ status: learningRecord.status, count: count() }) + .from(learningRecord) + .where(recordWhere) + .groupBy(learningRecord.status), + db + .select({ recordedBy: learningRecord.recordedBy, count: count() }) + .from(learningRecord) + .where(recordWhere) + .groupBy(learningRecord.recordedBy), ]) const stats = { total, - planned: statusGroups.find((group) => group.status === 'PLANNED')?._count._all ?? 0, - inProgress: statusGroups.find((group) => group.status === 'IN_PROGRESS')?._count._all ?? 0, - completed: statusGroups.find((group) => group.status === 'COMPLETED')?._count._all ?? 0, - residentLogged: sourceGroups.find((group) => group.recordedBy === 'RESIDENT')?._count._all ?? 0, - staffLogged: sourceGroups.find((group) => group.recordedBy === 'STAFF')?._count._all ?? 0, + planned: statusGroups.find((group) => group.status === 'PLANNED')?.count ?? 0, + inProgress: statusGroups.find((group) => group.status === 'IN_PROGRESS')?.count ?? 0, + completed: statusGroups.find((group) => group.status === 'COMPLETED')?.count ?? 0, + residentLogged: sourceGroups.find((group) => group.recordedBy === 'RESIDENT')?.count ?? 0, + staffLogged: sourceGroups.find((group) => group.recordedBy === 'STAFF')?.count ?? 0, } return { user, records, missingGerman, stats } @@ -269,11 +294,13 @@ export async function listResidentLearningEvidence() { const code = await getResidentCookie() if (!code) return null - return prisma.resident.findUnique({ - where: { code }, - select: { - id: true, - learningRecords: { orderBy: { updatedAt: 'desc' } }, - }, - }) + return ( + (await db.query.resident.findFirst({ + where: eq(resident.code, code), + columns: { id: true }, + with: { + learningRecords: { orderBy: [desc(learningRecord.updatedAt)] }, + }, + })) ?? null + ) } diff --git a/src/lib/actions/maintenance.ts b/src/lib/actions/maintenance.ts index dfd6a203..38fa5a5a 100644 --- a/src/lib/actions/maintenance.ts +++ b/src/lib/actions/maintenance.ts @@ -2,7 +2,8 @@ import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, maintenanceRequest } from '@/lib/db' +import { and, desc, eq, gte, inArray } from 'drizzle-orm' import { validateFormData, MaintenanceRequestInputSchema, @@ -27,8 +28,9 @@ export async function createMaintenanceRequest(formData: FormData): Promise const data = validateFormData(MaintenanceStatusUpdateSchema, formData) try { - const updateData: Record = { status: data.status } + const updateData: Partial = { status: data.status } // Set timestamps based on status if (data.status === 'ASSIGNED' && data.assignedTo) { @@ -87,11 +89,13 @@ export async function updateMaintenanceStatus(formData: FormData): Promise } if (data.notes) updateData.notes = data.notes - const request = await prisma.maintenanceRequest.update({ - where: { id: data.requestId }, - data: updateData, - select: { housingUnitId: true }, - }) + const [request] = await db + .update(maintenanceRequest) + .set(updateData) + .where(eq(maintenanceRequest.id, data.requestId)) + .returning({ housingUnitId: maintenanceRequest.housingUnitId }) + // Prisma threw when the request was missing — keep that error path + if (!request) throw new Error(ERROR_MESSAGES.MAINTENANCE_STATUS_UPDATE_ERROR) await logAudit({ action: 'UPDATE', @@ -117,15 +121,17 @@ export async function assignMaintenanceRequest(formData: FormData): Promise[0] + /** * The board as one reader sees it. * @@ -79,17 +88,17 @@ export async function listPortalMarketplacePosts(nature?: MarketplaceNature): Pr const auth = await getPortalAuth() if (!auth) return null - const rows = await prisma.marketplacePost.findMany({ - where: { hiddenByStaff: false }, - include: POST_INCLUDE, - orderBy: { createdAt: 'desc' }, + const rows = await db.query.marketplacePost.findMany({ + where: eq(marketplacePost.hiddenByStaff, false), + with: POST_INCLUDE, + orderBy: [desc(marketplacePost.createdAt)], }) const mine = auth.resident.id const unitId = auth.placement.housingUnitId const mapped = rows .map((row) => - mapPost(row, { + mapPost(row as unknown as PostRow, { // Contact details reach the two people the handover is between, and // nobody else. Computed here, once, from the reader's identity. canSeeContact: row.postedById === mine || row.claimedById === mine, @@ -120,28 +129,28 @@ export async function listMyMarketplacePosts(): Promise mapPost(row, { canSeeContact: true })) + return rows.map((row) => mapPost(row as unknown as PostRow, { canSeeContact: true })) } export async function listStaffMarketplacePosts(): Promise { - const rows = await prisma.marketplacePost.findMany({ - include: POST_INCLUDE, - orderBy: { createdAt: 'desc' }, + const rows = await db.query.marketplacePost.findMany({ + with: POST_INCLUDE, + orderBy: [desc(marketplacePost.createdAt)], }) // Staff moderate the board, so they read it whole — that is the job. - return rows.map((row) => mapPost(row, { canSeeContact: true })) + return rows.map((row) => mapPost(row as unknown as PostRow, { canSeeContact: true })) } function mapPost( @@ -207,16 +216,14 @@ export async function createMarketplacePost( const requested = parseCategory(formData.get('category')) const category = categoryFitsKind(kind, requested) ? requested : 'OTHER' - await prisma.marketplacePost.create({ - data: { - housingUnitId: auth.placement.housingUnitId, - postedById: auth.resident.id, - title, - description, - contactNote, - kind, - category, - }, + await db.insert(marketplacePost).values({ + housingUnitId: auth.placement.housingUnitId, + postedById: auth.resident.id, + title, + description, + contactNote, + kind, + category, }) revalidateMarketplace() @@ -230,7 +237,7 @@ export async function claimMarketplacePost( if (!auth) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } const id = String(formData.get('id') || '') - const post = await prisma.marketplacePost.findUnique({ where: { id } }) + const post = await db.query.marketplacePost.findFirst({ where: eq(marketplacePost.id, id) }) if (!post || post.status !== 'OPEN' || post.hiddenByStaff) { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } @@ -245,11 +252,12 @@ export async function claimMarketplacePost( // Conditional on still being OPEN, so two people pressing at once produces // one winner rather than a silent overwrite of the first claim. - const claimed = await prisma.marketplacePost.updateMany({ - where: { id, status: 'OPEN' }, - data: { status: 'CLAIMED', claimedById: auth.resident.id, claimedAt: new Date() }, - }) - if (claimed.count === 0) { + const claimed = await db + .update(marketplacePost) + .set({ status: 'CLAIMED', claimedById: auth.resident.id, claimedAt: new Date() }) + .where(and(eq(marketplacePost.id, id), eq(marketplacePost.status, 'OPEN'))) + .returning({ id: marketplacePost.id }) + if (claimed.length === 0) { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } @@ -271,7 +279,7 @@ export async function releaseMarketplaceClaim( if (!auth) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } const id = String(formData.get('id') || '') - const post = await prisma.marketplacePost.findUnique({ where: { id } }) + const post = await db.query.marketplacePost.findFirst({ where: eq(marketplacePost.id, id) }) if (!post || post.status !== 'CLAIMED') { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } @@ -282,10 +290,10 @@ export async function releaseMarketplaceClaim( return { success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS } } - await prisma.marketplacePost.update({ - where: { id }, - data: { status: 'OPEN', claimedById: null, claimedAt: null }, - }) + await db + .update(marketplacePost) + .set({ status: 'OPEN', claimedById: null, claimedAt: null }) + .where(eq(marketplacePost.id, id)) revalidateMarketplace() return { success: true } @@ -298,15 +306,15 @@ export async function closeMarketplacePost( if (!auth) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } const id = String(formData.get('id') || '') - const post = await prisma.marketplacePost.findUnique({ where: { id } }) + const post = await db.query.marketplacePost.findFirst({ where: eq(marketplacePost.id, id) }) if (!post) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } const isOwner = post.postedById === auth.resident.id || post.claimedById === auth.resident.id if (!isOwner) return { success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS } - await prisma.marketplacePost.update({ - where: { id }, - data: { status: 'CLOSED', closedAt: new Date() }, - }) + await db + .update(marketplacePost) + .set({ status: 'CLOSED', closedAt: new Date() }) + .where(eq(marketplacePost.id, id)) revalidateMarketplace() return { success: true } @@ -320,7 +328,7 @@ export async function reopenMarketplacePost( if (!auth) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } const id = String(formData.get('id') || '') - const post = await prisma.marketplacePost.findUnique({ where: { id } }) + const post = await db.query.marketplacePost.findFirst({ where: eq(marketplacePost.id, id) }) if (!post || post.status === 'OPEN') { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } @@ -328,10 +336,10 @@ export async function reopenMarketplacePost( return { success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS } } - await prisma.marketplacePost.update({ - where: { id }, - data: { status: 'OPEN', claimedById: null, claimedAt: null, closedAt: null }, - }) + await db + .update(marketplacePost) + .set({ status: 'OPEN', claimedById: null, claimedAt: null, closedAt: null }) + .where(eq(marketplacePost.id, id)) revalidateMarketplace() return { success: true } @@ -350,13 +358,13 @@ export async function deleteMarketplacePost( if (!auth) return { success: false, error: ERROR_MESSAGES.NOT_AUTHENTICATED } const id = String(formData.get('id') || '') - const post = await prisma.marketplacePost.findUnique({ where: { id } }) + const post = await db.query.marketplacePost.findFirst({ where: eq(marketplacePost.id, id) }) if (!post) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } if (post.postedById !== auth.resident.id || post.status !== 'OPEN') { return { success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS } } - await prisma.marketplacePost.delete({ where: { id } }) + await db.delete(marketplacePost).where(eq(marketplacePost.id, id)) revalidateMarketplace() return { success: true } @@ -375,10 +383,13 @@ export async function hideMarketplacePost( const reason = String(formData.get('reason') || '').trim() || null if (!id) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } - await prisma.marketplacePost.update({ - where: { id }, - data: { hiddenByStaff: true, hiddenReason: reason }, - }) + const hidden = await db + .update(marketplacePost) + .set({ hiddenByStaff: true, hiddenReason: reason }) + .where(eq(marketplacePost.id, id)) + .returning({ id: marketplacePost.id }) + // Prisma's update threw when the id matched no row; keep that error path. + if (hidden.length === 0) throw new Error(ERROR_MESSAGES.SAVE_ERROR) revalidateMarketplace() return { success: true } @@ -396,10 +407,13 @@ export async function unhideMarketplacePost( const id = String(formData.get('id') || '') if (!id) return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } - await prisma.marketplacePost.update({ - where: { id }, - data: { hiddenByStaff: false, hiddenReason: null }, - }) + const unhidden = await db + .update(marketplacePost) + .set({ hiddenByStaff: false, hiddenReason: null }) + .where(eq(marketplacePost.id, id)) + .returning({ id: marketplacePost.id }) + // Prisma's update threw when the id matched no row; keep that error path. + if (unhidden.length === 0) throw new Error(ERROR_MESSAGES.SAVE_ERROR) revalidateMarketplace() return { success: true } diff --git a/src/lib/actions/matching.ts b/src/lib/actions/matching.ts index 18e1dafc..b0ca0de6 100644 --- a/src/lib/actions/matching.ts +++ b/src/lib/actions/matching.ts @@ -1,6 +1,14 @@ 'use server' -import { prisma } from '@/lib/db' +import { and, eq } from 'drizzle-orm' +import { + db, + housingUnit, + placement as placementTable, + placementSpot, + resident as residentTable, + type Resident, +} from '@/lib/db' import { redirect } from 'next/navigation' import { logAudit } from '@/lib/audit' import { calculateCompatibility, saveBidirectionalAssessment } from '@/lib/compatibility' @@ -84,12 +92,12 @@ export async function placeResident(formData: FormData) { // Execute all placement operations in a transaction to ensure atomicity let placement try { - placement = await prisma.$transaction(async (tx) => { + placement = await db.transaction(async (tx) => { // 1. Check if spot is still available (prevents double-booking) if (spotId) { - const spot = await tx.placementSpot.findUnique({ - where: { id: spotId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const spot = await tx.query.placementSpot.findFirst({ + where: eq(placementSpot.id, spotId), + with: { placements: { where: eq(placementTable.status, 'ACTIVE') } }, }) if (!spot) { @@ -106,9 +114,9 @@ export async function placeResident(formData: FormData) { } // 2. Check if resident is still unplaced - const resident = await tx.resident.findUnique({ - where: { id: residentId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const resident = await tx.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + with: { placements: { where: eq(placementTable.status, 'ACTIVE') } }, }) if (!resident) { @@ -120,14 +128,21 @@ export async function placeResident(formData: FormData) { } // 3. Fetch existing active placements in the unit for score calculation - const existingPlacements = await tx.placement.findMany({ - where: { housingUnitId, status: 'ACTIVE' }, - include: { resident: true }, + const existingPlacements = await tx.query.placement.findMany({ + where: and( + eq(placementTable.housingUnitId, housingUnitId), + eq(placementTable.status, 'ACTIVE'), + ), + with: { resident: true }, }) // 4. Server-side blocking conflict check (never trust client) + // (`db.query`'s relation typing collapses to an untyped fallback for + // this schema; at runtime each `p.resident` is one Resident row.) const residentProfile = toResidentProfile(resident) - const existingProfiles = existingPlacements.map((p) => toResidentProfile(p.resident)) + const existingProfiles = existingPlacements.map((p) => + toResidentProfile(p.resident as unknown as Resident), + ) const apartmentProfile = calculateApartmentProfile(existingProfiles) const apartmentFit = calculateApartmentFit(residentProfile, apartmentProfile) const apartmentFitScore = apartmentFit.fitScore @@ -154,7 +169,7 @@ export async function placeResident(formData: FormData) { if (existingPlacements.length > 0) { for (const existingPlacement of existingPlacements) { - const otherProfile = toResidentProfile(existingPlacement.resident) + const otherProfile = toResidentProfile(existingPlacement.resident as unknown as Resident) const score = calculateCompatibility(residentProfile, otherProfile) // Collect insights @@ -180,8 +195,9 @@ export async function placeResident(formData: FormData) { ) // 7. Create placement with server-computed scores - const newPlacement = await tx.placement.create({ - data: { + const [newPlacement] = await tx + .insert(placementTable) + .values({ residentId, housingUnitId, spotId, @@ -193,35 +209,35 @@ export async function placeResident(formData: FormData) { practicalScore, riskScore, placementNotes, - }, - }) + }) + .returning() // 8. Update spot status if assigned if (spotId) { - await tx.placementSpot.update({ - where: { id: spotId }, - data: { status: 'OCCUPIED' }, - }) + await tx + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, spotId)) } // 9. Update resident status - await tx.resident.update({ - where: { id: residentId }, - data: { status: 'PLACED' }, - }) + await tx + .update(residentTable) + .set({ status: 'PLACED' }) + .where(eq(residentTable.id, residentId)) // 10. Check if unit is now full and update status - const unit = await tx.housingUnit.findUnique({ - where: { id: housingUnitId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const unit = await tx.query.housingUnit.findFirst({ + where: eq(housingUnit.id, housingUnitId), + with: { placements: { where: eq(placementTable.status, 'ACTIVE') } }, }) // placements already includes the newly created one (same transaction) if (unit && unit.placements.length >= unit.totalBeds) { - await tx.housingUnit.update({ - where: { id: housingUnitId }, - data: { status: 'FULL' }, - }) + await tx + .update(housingUnit) + .set({ status: 'FULL' }) + .where(eq(housingUnit.id, housingUnitId)) } return { diff --git a/src/lib/actions/opportunities.ts b/src/lib/actions/opportunities.ts index 6533c6e0..4beecd36 100644 --- a/src/lib/actions/opportunities.ts +++ b/src/lib/actions/opportunities.ts @@ -2,13 +2,21 @@ import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' -import { Prisma } from '@prisma/client' +import { eq } from 'drizzle-orm' import { requirePermission } from '@/lib/auth' import { getResidentCookie } from '@/lib/portal-auth' import { isFull } from '@/lib/opportunities/pipeline' import { logAudit } from '@/lib/audit' import { logger } from '@/lib/logger' -import { prisma } from '@/lib/db' +import { + db, + isUniqueViolation, + learningRecord, + opportunity as opportunityTable, + opportunityApplication, + resident as residentTable, + type Opportunity, +} from '@/lib/db' import { ApplicationCreateSchema, ApplicationStageChangeSchema, @@ -43,13 +51,15 @@ export async function createOpportunity(formData: FormData): Promise { let created try { - created = await prisma.opportunity.create({ - data: { + const [row] = await db + .insert(opportunityTable) + .values({ ...nullifyBlanks(data), createdByUserId: user.id, updatedByUserId: user.id, - }, - }) + }) + .returning() + created = row await logAudit({ action: 'CREATE', @@ -72,10 +82,13 @@ export async function updateOpportunity(formData: FormData): Promise { const { id, ...data } = validateFormData(OpportunityUpdateSchema, formData) try { - await prisma.opportunity.update({ - where: { id }, - data: { ...nullifyBlanks(data), updatedByUserId: user.id }, - }) + const [updated] = await db + .update(opportunityTable) + .set({ ...nullifyBlanks(data), updatedByUserId: user.id }) + .where(eq(opportunityTable.id, id)) + .returning({ id: opportunityTable.id }) + // Prisma's update threw when the id matched no row; keep that error path. + if (!updated) throw new Error('Einsatzplatz nicht gefunden') await logAudit({ action: 'UPDATE', @@ -102,9 +115,9 @@ async function setStatus(opportunityId: string, status: OpportunityStatusId): Pr // could go live one click later still claiming no authorisation is needed — // the gate would exist and be trivially walked around. if (status === 'PUBLISHED') { - const existing = await prisma.opportunity.findUnique({ - where: { id: opportunityId }, - select: { kind: true, permitRequirement: true }, + const existing = await db.query.opportunity.findFirst({ + where: eq(opportunityTable.id, opportunityId), + columns: { kind: true, permitRequirement: true }, }) if (!existing) throw new Error('Einsatzplatz nicht gefunden') if (!permitRequirementIsStated(existing.kind, existing.permitRequirement)) { @@ -116,10 +129,13 @@ async function setStatus(opportunityId: string, status: OpportunityStatusId): Pr } try { - await prisma.opportunity.update({ - where: { id: opportunityId }, - data: { status, updatedByUserId: user.id }, - }) + const [updated] = await db + .update(opportunityTable) + .set({ status, updatedByUserId: user.id }) + .where(eq(opportunityTable.id, opportunityId)) + .returning({ id: opportunityTable.id }) + // Prisma's update threw when the id matched no row; keep that error path. + if (!updated) throw new Error('Einsatzplatz nicht gefunden') await logAudit({ action: status === 'ARCHIVED' ? 'ARCHIVE' : 'UPDATE', @@ -149,17 +165,15 @@ export async function addApplicant(formData: FormData): Promise { const data = validateFormData(ApplicationCreateSchema, formData) try { - await prisma.opportunityApplication.create({ - data: { - opportunityId: data.opportunityId, - residentId: data.residentId, - note: data.note || null, - stage: 'INTERESTED', - // Staff put this person forward. The resident portal will set - // 'RESIDENT' for self-service interest in the next phase. - createdBy: 'STAFF', - supportedByUserId: user.id, - }, + await db.insert(opportunityApplication).values({ + opportunityId: data.opportunityId, + residentId: data.residentId, + note: data.note || null, + stage: 'INTERESTED', + // Staff put this person forward. The resident portal will set + // 'RESIDENT' for self-service interest in the next phase. + createdBy: 'STAFF', + supportedByUserId: user.id, }) await logAudit({ @@ -192,16 +206,16 @@ export async function changeApplicationStage(formData: FormData): Promise const user = await requirePermission('opportunities:write') const { applicationId, stage, hours } = validateFormData(ApplicationStageChangeSchema, formData) - const application = await prisma.opportunityApplication.findUnique({ - where: { id: applicationId }, - include: { opportunity: true }, + const application = await db.query.opportunityApplication.findFirst({ + where: eq(opportunityApplication.id, applicationId), + with: { opportunity: true }, }) if (!application) throw new Error('Bewerbung nicht gefunden') const now = new Date() try { - await prisma.$transaction(async (tx) => { + await db.transaction(async (tx) => { let learningRecordId = application.learningRecordId // Generate the evidence exactly once. A coach correcting a misclick @@ -209,37 +223,43 @@ export async function changeApplicationStage(formData: FormData): Promise // `learningRecordId` is unique, so the second insert would throw and // the stage move would fail for a reason nobody could act on. if (stage === 'STARTED' && !learningRecordId) { - const record = await tx.learningRecord.create({ - data: { + const [record] = await tx + .insert(learningRecord) + .values({ residentId: application.residentId, - ...evidenceForStartedApplication(application.opportunity, now), - }, - }) + // (`db.query`'s relation typing collapses to an untyped fallback + // for this schema; at runtime `.opportunity` is one row.) + ...evidenceForStartedApplication( + application.opportunity as unknown as Opportunity, + now, + ), + }) + .returning({ id: learningRecord.id }) learningRecordId = record.id } // The total is only knowable when the engagement is over, which is why // it is asked for here and never derived from hoursPerWeek. if (stage === 'ENDED' && learningRecordId) { - await tx.learningRecord.update({ - where: { id: learningRecordId }, - data: { + await tx + .update(learningRecord) + .set({ status: 'COMPLETED', completedAt: now, ...(hours !== null ? { hours } : {}), - }, - }) + }) + .where(eq(learningRecord.id, learningRecordId)) } - await tx.opportunityApplication.update({ - where: { id: applicationId }, - data: { + await tx + .update(opportunityApplication) + .set({ stage, stageChangedAt: now, learningRecordId, supportedByUserId: application.supportedByUserId ?? user.id, - }, - }) + }) + .where(eq(opportunityApplication.id, applicationId)) }) await logAudit({ @@ -292,18 +312,18 @@ async function actingResidentId(): Promise { const code = await getResidentCookie() if (!code) return null - const resident = await prisma.resident.findUnique({ - where: { code }, - select: { id: true }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.code, code), + columns: { id: true }, }) return resident?.id ?? null } /** All the fallible work, so `redirect()` never runs inside a `try`. */ async function recordInterest(opportunityId: string, residentId: string): Promise { - const opportunity = await prisma.opportunity.findUnique({ - where: { id: opportunityId }, - include: { applications: { select: { stage: true } } }, + const opportunity = await db.query.opportunity.findFirst({ + where: eq(opportunityTable.id, opportunityId), + with: { applications: { columns: { stage: true } } }, }) // A DRAFT is a listing staff are still writing and an ARCHIVED one is over. @@ -319,21 +339,19 @@ async function recordInterest(opportunityId: string, residentId: string): Promis return 'error=full' try { - await prisma.opportunityApplication.create({ - data: { - opportunityId, - residentId, - stage: 'INTERESTED', - // The resident put themselves forward. `supportedByUserId` stays null - // on purpose: it is the honest record that nobody on the staff side has - // picked this up yet, which is exactly what the queue is filtering for. - createdBy: 'RESIDENT', - }, + await db.insert(opportunityApplication).values({ + opportunityId, + residentId, + stage: 'INTERESTED', + // The resident put themselves forward. `supportedByUserId` stays null + // on purpose: it is the honest record that nobody on the staff side has + // picked this up yet, which is exactly what the queue is filtering for. + createdBy: 'RESIDENT', }) } catch (error) { // Already attached. That IS the state they asked for, so reporting a // failure would be a lie about a button that worked the first time. - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { return 'ok=interest' } logger.errorWithCause('Failed to record resident interest', error, { opportunityId }) @@ -378,9 +396,9 @@ export async function expressInterest(formData: FormData): Promise { * your team instead of failing silently. */ async function removeInterest(applicationId: string, residentId: string): Promise { - const application = await prisma.opportunityApplication.findUnique({ - where: { id: applicationId }, - select: { id: true, residentId: true, opportunityId: true, createdBy: true, stage: true }, + const application = await db.query.opportunityApplication.findFirst({ + where: eq(opportunityApplication.id, applicationId), + columns: { id: true, residentId: true, opportunityId: true, createdBy: true, stage: true }, }) // Same answer for "not yours" as for "does not exist": a distinguishable @@ -391,7 +409,7 @@ async function removeInterest(applicationId: string, residentId: string): Promis } try { - await prisma.opportunityApplication.delete({ where: { id: applicationId } }) + await db.delete(opportunityApplication).where(eq(opportunityApplication.id, applicationId)) } catch (error) { logger.errorWithCause('Failed to withdraw resident interest', error, { applicationId }) return 'error=failed' diff --git a/src/lib/actions/placements.ts b/src/lib/actions/placements.ts index b0ff00f5..de8b3039 100644 --- a/src/lib/actions/placements.ts +++ b/src/lib/actions/placements.ts @@ -1,6 +1,14 @@ 'use server' -import { prisma } from '@/lib/db' +import { and, eq, ne } from 'drizzle-orm' +import { + db, + housingUnit, + placement as placementTable, + placementSpot, + resident as residentTable, + type Resident, +} from '@/lib/db' import { logger } from '@/lib/logger' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' @@ -28,23 +36,27 @@ export async function createPlacement( const { residentId, housingUnitId, spotId, startDate, notes } = input try { - const placement = await prisma.$transaction(async (tx) => { + const placement = await db.transaction(async (tx) => { // 1. Validate resident exists - const resident = await tx.resident.findUnique({ where: { id: residentId } }) + const resident = await tx.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + }) if (!resident) { throw new Error(ERROR_MESSAGES.RESIDENT_NOT_FOUND) } // 2. Prevent duplicate active placement - const existingActivePlacement = await tx.placement.findFirst({ - where: { residentId, status: 'ACTIVE' }, + const existingActivePlacement = await tx.query.placement.findFirst({ + where: and(eq(placementTable.residentId, residentId), eq(placementTable.status, 'ACTIVE')), }) if (existingActivePlacement) { throw new Error(ERROR_MESSAGES.RESIDENT_HAS_ACTIVE_PLACEMENT) } // 3. Validate spot is available - const spot = await tx.placementSpot.findUnique({ where: { id: spotId } }) + const spot = await tx.query.placementSpot.findFirst({ + where: eq(placementSpot.id, spotId), + }) if (!spot) { throw new Error(ERROR_MESSAGES.SPOT_NOT_FOUND) } @@ -53,9 +65,12 @@ export async function createPlacement( } // 4. Calculate compatibility scores with existing residents - const existingPlacements = await tx.placement.findMany({ - where: { housingUnitId, status: 'ACTIVE' }, - include: { resident: true }, + const existingPlacements = await tx.query.placement.findMany({ + where: and( + eq(placementTable.housingUnitId, housingUnitId), + eq(placementTable.status, 'ACTIVE'), + ), + with: { resident: true }, }) const { compatibilityScore, lifestyleScore, socialScore, practicalScore, riskScore } = @@ -66,15 +81,18 @@ export async function createPlacement( const residentProfile = toResidentProfile(resident) for (const existingPlacement of existingPlacements) { - const otherProfile = toResidentProfile(existingPlacement.resident) + // (`db.query`'s relation typing collapses to an untyped fallback for + // this schema; at runtime `.resident` is one Resident row.) + const otherProfile = toResidentProfile(existingPlacement.resident as unknown as Resident) const score = calculateCompatibility(residentProfile, otherProfile) await saveBidirectionalAssessment(tx, residentId, existingPlacement.residentId, score) } } // 6. Create the placement - const newPlacement = await tx.placement.create({ - data: { + const [newPlacement] = await tx + .insert(placementTable) + .values({ residentId, housingUnitId, spotId, @@ -86,33 +104,32 @@ export async function createPlacement( practicalScore, riskScore, placementNotes: notes, - }, - }) + }) + .returning() // 7. Mark spot as occupied - await tx.placementSpot.update({ - where: { id: spotId }, - data: { status: 'OCCUPIED' }, - }) + await tx.update(placementSpot).set({ status: 'OCCUPIED' }).where(eq(placementSpot.id, spotId)) // 8. Update resident status - await tx.resident.update({ - where: { id: residentId }, - data: { status: 'PLACED' }, - }) + await tx + .update(residentTable) + .set({ status: 'PLACED' }) + .where(eq(residentTable.id, residentId)) // 9. Check if housing unit is now full - const unit = await tx.housingUnit.findUnique({ - where: { id: housingUnitId }, - include: { - spots: { where: { status: 'AVAILABLE', type: { not: 'ROOM' } } }, + const unit = await tx.query.housingUnit.findFirst({ + where: eq(housingUnit.id, housingUnitId), + with: { + spots: { + where: and(eq(placementSpot.status, 'AVAILABLE'), ne(placementSpot.type, 'ROOM')), + }, }, }) if (unit && unit.spots.length === 0) { - await tx.housingUnit.update({ - where: { id: housingUnitId }, - data: { status: 'FULL' }, - }) + await tx + .update(housingUnit) + .set({ status: 'FULL' }) + .where(eq(housingUnit.id, housingUnitId)) } return newPlacement @@ -156,10 +173,10 @@ export async function endPlacement(formData: FormData): Promise { } = validateFormData(EndPlacementSchema, formData) try { - await prisma.$transaction(async (tx) => { - const currentPlacement = await tx.placement.findUnique({ - where: { id: placementId }, - include: { spot: true }, + await db.transaction(async (tx) => { + const currentPlacement = await tx.query.placement.findFirst({ + where: eq(placementTable.id, placementId), + with: { spot: true }, }) if (!currentPlacement || currentPlacement.status !== 'ACTIVE') { @@ -190,35 +207,32 @@ export async function endPlacement(formData: FormData): Promise { if (relatedIncidentId) updateData.relatedIncidentId = relatedIncidentId } - await tx.placement.update({ - where: { id: placementId }, - data: updateData, - }) + await tx.update(placementTable).set(updateData).where(eq(placementTable.id, placementId)) // Free up the old spot if there was one if (currentPlacement.spotId) { - await tx.placementSpot.update({ - where: { id: currentPlacement.spotId }, - data: { status: 'AVAILABLE' }, - }) + await tx + .update(placementSpot) + .set({ status: 'AVAILABLE' }) + .where(eq(placementSpot.id, currentPlacement.spotId)) } // Update resident status back to ACTIVE (unplaced) - await tx.resident.update({ - where: { id: residentId }, - data: { status: 'ACTIVE' }, - }) + await tx + .update(residentTable) + .set({ status: 'ACTIVE' }) + .where(eq(residentTable.id, residentId)) // Update housing unit status if it was full if (currentPlacement.housingUnitId) { - const unit = await tx.housingUnit.findUnique({ - where: { id: currentPlacement.housingUnitId }, + const unit = await tx.query.housingUnit.findFirst({ + where: eq(housingUnit.id, currentPlacement.housingUnitId), }) if (unit?.status === 'FULL') { - await tx.housingUnit.update({ - where: { id: currentPlacement.housingUnitId }, - data: { status: 'AVAILABLE' }, - }) + await tx + .update(housingUnit) + .set({ status: 'AVAILABLE' }) + .where(eq(housingUnit.id, currentPlacement.housingUnitId)) } } }) @@ -261,11 +275,11 @@ export async function transferPlacement(formData: FormData): Promise { let fromHousingUnitId: string try { - fromHousingUnitId = await prisma.$transaction(async (tx) => { + fromHousingUnitId = await db.transaction(async (tx) => { // 1. Get current placement to access spot - const currentPlacement = await tx.placement.findUnique({ - where: { id: currentPlacementId }, - include: { spot: true }, + const currentPlacement = await tx.query.placement.findFirst({ + where: eq(placementTable.id, currentPlacementId), + with: { spot: true }, }) if (!currentPlacement) { @@ -276,37 +290,44 @@ export async function transferPlacement(formData: FormData): Promise { } // 2. Validate target spot is available - const targetSpot = await tx.placementSpot.findUnique({ where: { id: targetSpotId } }) + const targetSpot = await tx.query.placementSpot.findFirst({ + where: eq(placementSpot.id, targetSpotId), + }) if (!targetSpot || targetSpot.status !== 'AVAILABLE') { throw new Error('Target spot is not available') } // 3. End current placement with TRANSFERRED status - await tx.placement.update({ - where: { id: currentPlacementId }, - data: { + await tx + .update(placementTable) + .set({ status: 'TRANSFERRED', endDate: new Date(), endReason: transferReason, outcomeNotes: notes || undefined, - }, - }) + }) + .where(eq(placementTable.id, currentPlacementId)) // 4. Free up the old spot if (currentPlacement.spotId) { - await tx.placementSpot.update({ - where: { id: currentPlacement.spotId }, - data: { status: 'AVAILABLE' }, - }) + await tx + .update(placementSpot) + .set({ status: 'AVAILABLE' }) + .where(eq(placementSpot.id, currentPlacement.spotId)) } // 5. Calculate compatibility scores with existing residents at target - const resident = await tx.resident.findUnique({ where: { id: residentId } }) + const resident = await tx.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + }) if (!resident) throw new Error('Resident not found') - const targetResidents = await tx.placement.findMany({ - where: { housingUnitId: targetHousingUnitId, status: 'ACTIVE' }, - include: { resident: true }, + const targetResidents = await tx.query.placement.findMany({ + where: and( + eq(placementTable.housingUnitId, targetHousingUnitId), + eq(placementTable.status, 'ACTIVE'), + ), + with: { resident: true }, }) const { compatibilityScore, lifestyleScore, socialScore, practicalScore, riskScore } = @@ -317,57 +338,59 @@ export async function transferPlacement(formData: FormData): Promise { const residentProfile = toResidentProfile(resident) for (const existingPlacement of targetResidents) { - const otherProfile = toResidentProfile(existingPlacement.resident) + // (`db.query`'s relation typing collapses to an untyped fallback for + // this schema; at runtime `.resident` is one Resident row.) + const otherProfile = toResidentProfile(existingPlacement.resident as unknown as Resident) const score = calculateCompatibility(residentProfile, otherProfile) await saveBidirectionalAssessment(tx, residentId, existingPlacement.residentId, score) } } // 7. Create new placement at target with calculated scores - await tx.placement.create({ - data: { - residentId, - housingUnitId: targetHousingUnitId, - spotId: targetSpotId, - startDate: new Date(), - status: 'ACTIVE', - compatibilityScore, - lifestyleScore, - socialScore, - practicalScore, - riskScore, - placementNotes: `Verlegt von ${currentPlacement.housingUnitId}. ${notes || ''}`.trim(), - }, + await tx.insert(placementTable).values({ + residentId, + housingUnitId: targetHousingUnitId, + spotId: targetSpotId, + startDate: new Date(), + status: 'ACTIVE', + compatibilityScore, + lifestyleScore, + socialScore, + practicalScore, + riskScore, + placementNotes: `Verlegt von ${currentPlacement.housingUnitId}. ${notes || ''}`.trim(), }) // 8. Mark new spot as occupied - await tx.placementSpot.update({ - where: { id: targetSpotId }, - data: { status: 'OCCUPIED' }, - }) + await tx + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, targetSpotId)) // 9. Update housing unit statuses - const oldUnit = await tx.housingUnit.findUnique({ - where: { id: currentPlacement.housingUnitId }, + const oldUnit = await tx.query.housingUnit.findFirst({ + where: eq(housingUnit.id, currentPlacement.housingUnitId), }) if (oldUnit?.status === 'FULL') { - await tx.housingUnit.update({ - where: { id: currentPlacement.housingUnitId }, - data: { status: 'AVAILABLE' }, - }) + await tx + .update(housingUnit) + .set({ status: 'AVAILABLE' }) + .where(eq(housingUnit.id, currentPlacement.housingUnitId)) } - const newUnit = await tx.housingUnit.findUnique({ - where: { id: targetHousingUnitId }, - include: { - spots: { where: { status: 'AVAILABLE', type: { not: 'ROOM' } } }, + const newUnit = await tx.query.housingUnit.findFirst({ + where: eq(housingUnit.id, targetHousingUnitId), + with: { + spots: { + where: and(eq(placementSpot.status, 'AVAILABLE'), ne(placementSpot.type, 'ROOM')), + }, }, }) if (newUnit && newUnit.spots.length === 0) { - await tx.housingUnit.update({ - where: { id: targetHousingUnitId }, - data: { status: 'FULL' }, - }) + await tx + .update(housingUnit) + .set({ status: 'FULL' }) + .where(eq(housingUnit.id, targetHousingUnitId)) } return currentPlacement.housingUnitId diff --git a/src/lib/actions/residents.ts b/src/lib/actions/residents.ts index 9426c110..714336b8 100644 --- a/src/lib/actions/residents.ts +++ b/src/lib/actions/residents.ts @@ -1,6 +1,16 @@ 'use server' -import { prisma } from '@/lib/db' +import { + db, + resident as residentTable, + placement, + incident, + incidentInvolvement, + maintenanceRequest, + compatibilityAssessment, + isUniqueViolation, +} from '@/lib/db' +import { eq, or } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { validateFormData, ResidentInputSchema, ResidentUpdateSchema } from '@/lib/validation' @@ -9,7 +19,6 @@ import { logger } from '@/lib/logger' import { DEFAULT_STATUSES } from '@/lib/config/thresholds' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' import { requirePermission } from '@/lib/auth' -import { Prisma } from '@prisma/client' export async function createResident(formData: FormData): Promise { const user = await requirePermission('residents:write') @@ -17,12 +26,14 @@ export async function createResident(formData: FormData): Promise { let resident try { - resident = await prisma.resident.create({ - data: { + const [created] = await db + .insert(residentTable) + .values({ ...data, status: DEFAULT_STATUSES.resident, - }, - }) + }) + .returning() + resident = created await logAudit({ action: 'CREATE', @@ -32,7 +43,7 @@ export async function createResident(formData: FormData): Promise { changes: { code: data.code }, }) } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { throw new Error(ERROR_MESSAGES.RESIDENT_CODE_EXISTS) } logger.errorWithCause('Failed to create resident', error, { code: data.code }) @@ -50,9 +61,9 @@ export async function exitResident( ): Promise<{ success: boolean; error?: string }> { const user = await requirePermission('residents:write') try { - const resident = await prisma.resident.findUnique({ - where: { id: residentId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + with: { placements: { where: eq(placement.status, 'ACTIVE') } }, }) if (!resident) { @@ -63,10 +74,7 @@ export async function exitResident( return { success: false, error: ERROR_MESSAGES.RESIDENT_HAS_ACTIVE_PLACEMENTS } } - await prisma.resident.update({ - where: { id: residentId }, - data: { status: 'EXITED' }, - }) + await db.update(residentTable).set({ status: 'EXITED' }).where(eq(residentTable.id, residentId)) await logAudit({ action: 'END', @@ -92,10 +100,16 @@ export async function updateResident(formData: FormData): Promise { const { id, code: _code, ...updateData } = data try { - await prisma.resident.update({ - where: { id }, - data: updateData, - }) + const [updated] = await db + .update(residentTable) + .set(updateData) + .where(eq(residentTable.id, id)) + .returning({ id: residentTable.id }) + + // Updating a missing row used to throw (P2025); keep that error path. + if (!updated) { + throw new Error(ERROR_MESSAGES.RESIDENT_NOT_FOUND) + } await logAudit({ action: 'UPDATE', @@ -119,9 +133,9 @@ export async function archiveResident( ): Promise<{ success: boolean; error?: string }> { const user = await requirePermission('residents:write') try { - const resident = await prisma.resident.findUnique({ - where: { id: residentId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + with: { placements: { where: eq(placement.status, 'ACTIVE') } }, }) if (!resident) { @@ -132,10 +146,7 @@ export async function archiveResident( return { success: false, error: ERROR_MESSAGES.RESIDENT_ARCHIVE_BLOCKED } } - await prisma.resident.update({ - where: { id: residentId }, - data: { status: 'EXITED' }, - }) + await db.update(residentTable).set({ status: 'EXITED' }).where(eq(residentTable.id, residentId)) await logAudit({ action: 'ARCHIVE', @@ -159,9 +170,9 @@ export async function restoreResident( ): Promise<{ success: boolean; error?: string }> { const user = await requirePermission('residents:write') try { - const resident = await prisma.resident.findUnique({ - where: { id: residentId }, - include: { placements: { where: { status: 'ACTIVE' } } }, + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + with: { placements: { where: eq(placement.status, 'ACTIVE') } }, }) if (!resident) { @@ -170,10 +181,10 @@ export async function restoreResident( const nextStatus = resident.placements.length > 0 ? 'PLACED' : 'ACTIVE' - await prisma.resident.update({ - where: { id: residentId }, - data: { status: nextStatus }, - }) + await db + .update(residentTable) + .set({ status: nextStatus }) + .where(eq(residentTable.id, residentId)) await logAudit({ action: 'RESTORE', @@ -215,7 +226,9 @@ export async function hardDeleteResidentProtected( } } - const resident = await prisma.resident.findUnique({ where: { id: residentId } }) + const resident = await db.query.resident.findFirst({ + where: eq(residentTable.id, residentId), + }) if (!resident) { return { success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND } } @@ -232,16 +245,18 @@ export async function hardDeleteResidentProtected( maintenanceRequests, assessments, ] = await Promise.all([ - prisma.placement.count({ where: { residentId } }), - prisma.incident.count({ where: { reportedById: residentId } }), - prisma.incident.count({ where: { subjectId: residentId } }), - prisma.incidentInvolvement.count({ where: { residentId } }), - prisma.maintenanceRequest.count({ where: { reportedById: residentId } }), - prisma.compatibilityAssessment.count({ - where: { - OR: [{ residentId }, { comparedWithId: residentId }], - }, - }), + db.$count(placement, eq(placement.residentId, residentId)), + db.$count(incident, eq(incident.reportedById, residentId)), + db.$count(incident, eq(incident.subjectId, residentId)), + db.$count(incidentInvolvement, eq(incidentInvolvement.residentId, residentId)), + db.$count(maintenanceRequest, eq(maintenanceRequest.reportedById, residentId)), + db.$count( + compatibilityAssessment, + or( + eq(compatibilityAssessment.residentId, residentId), + eq(compatibilityAssessment.comparedWithId, residentId), + ), + ), ]) if ( @@ -267,7 +282,7 @@ export async function hardDeleteResidentProtected( } } - await prisma.resident.delete({ where: { id: residentId } }) + await db.delete(residentTable).where(eq(residentTable.id, residentId)) await logAudit({ action: 'DELETE', diff --git a/src/lib/actions/satisfaction.ts b/src/lib/actions/satisfaction.ts index 8dcdbb93..72b9f074 100644 --- a/src/lib/actions/satisfaction.ts +++ b/src/lib/actions/satisfaction.ts @@ -1,6 +1,7 @@ 'use server' -import { prisma } from '@/lib/db' +import { db, placement as placementTable, satisfactionCheckIn } from '@/lib/db' +import { eq, asc, desc } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { validateFormData, SatisfactionCheckInInputSchema } from '@/lib/validation' @@ -14,9 +15,9 @@ export async function createCheckInFromForm(formData: FormData): Promise { const user = await requirePermission('residents:write') const data = validateFormData(SatisfactionCheckInInputSchema, formData) - const placement = await prisma.placement.findUnique({ - where: { id: data.placementId }, - select: { residentId: true, startDate: true }, + const placement = await db.query.placement.findFirst({ + where: eq(placementTable.id, data.placementId), + columns: { residentId: true, startDate: true }, }) if (!placement) { @@ -29,9 +30,10 @@ export async function createCheckInFromForm(formData: FormData): Promise { // Wrap both writes in a transaction so a partial failure can't leave // the placement's cached rating out of sync with its check-in history. - const checkIn = await prisma.$transaction(async (tx) => { - const created = await tx.satisfactionCheckIn.create({ - data: { + const checkIn = await db.transaction(async (tx) => { + const [created] = await tx + .insert(satisfactionCheckIn) + .values({ placementId: data.placementId, checkInType: data.checkInType, weekNumber: data.weekNumber ?? weeksSinceStart, @@ -49,16 +51,16 @@ export async function createCheckInFromForm(formData: FormData): Promise { collectedBy: data.collectedBy || null, collectedByUserId: user.id, isAnonymous: data.isAnonymous ?? false, - }, - }) + }) + .returning() // Update placement satisfaction rating with latest overall - await tx.placement.update({ - where: { id: data.placementId }, - data: { + await tx + .update(placementTable) + .set({ satisfactionRating: data.overallSatisfaction, - }, - }) + }) + .where(eq(placementTable.id, data.placementId)) return created }) @@ -100,18 +102,18 @@ export async function createCheckInFromForm(formData: FormData): Promise { export async function getPlacementCheckIns(placementId: string) { await requirePermission('residents:read') - return prisma.satisfactionCheckIn.findMany({ - where: { placementId }, - orderBy: { createdAt: 'desc' }, + return db.query.satisfactionCheckIn.findMany({ + where: eq(satisfactionCheckIn.placementId, placementId), + orderBy: [desc(satisfactionCheckIn.createdAt)], }) } export async function getPlacementSatisfactionTrend(placementId: string) { await requirePermission('residents:read') - const checkIns = await prisma.satisfactionCheckIn.findMany({ - where: { placementId }, - orderBy: { createdAt: 'asc' }, - select: { + const checkIns = await db.query.satisfactionCheckIn.findMany({ + where: eq(satisfactionCheckIn.placementId, placementId), + orderBy: [asc(satisfactionCheckIn.createdAt)], + columns: { createdAt: true, weekNumber: true, overallSatisfaction: true, diff --git a/src/lib/actions/spots.ts b/src/lib/actions/spots.ts index 3bde59fd..5c8d53d0 100644 --- a/src/lib/actions/spots.ts +++ b/src/lib/actions/spots.ts @@ -1,6 +1,7 @@ 'use server' -import { prisma } from '@/lib/db' +import { db, placementSpot, placement, isUniqueViolation } from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { z } from 'zod' @@ -14,7 +15,6 @@ import { logger } from '@/lib/logger' import { DEFAULT_STATUSES } from '@/lib/config/thresholds' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' import { requirePermission } from '@/lib/auth' -import { Prisma } from '@prisma/client' // Simple schema for delete operation const DeleteSpotSchema = z.object({ @@ -27,22 +27,20 @@ export async function createSpot(formData: FormData): Promise { const data = validateFormData(SpotInputSchema, formData) try { - await prisma.placementSpot.create({ - data: { - housingUnitId: data.housingUnitId, - code: data.code, - label: data.label, - type: data.type, - parentSpotId: data.parentSpotId, - squareMeters: data.squareMeters, - floor: data.floor, - requiresMedicalDocs: data.requiresMedicalDocs, - status: data.status, - notes: data.notes, - }, + await db.insert(placementSpot).values({ + housingUnitId: data.housingUnitId, + code: data.code, + label: data.label, + type: data.type, + parentSpotId: data.parentSpotId, + squareMeters: data.squareMeters, + floor: data.floor, + requiresMedicalDocs: data.requiresMedicalDocs, + status: data.status, + notes: data.notes, }) } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { throw new Error(ERROR_MESSAGES.SPOT_CODE_EXISTS) } logger.errorWithCause('Failed to create spot', error, { housingUnitId: data.housingUnitId }) @@ -60,13 +58,19 @@ export async function updateSpot(formData: FormData): Promise { const { id, housingUnitId, ...updateData } = data try { - await prisma.placementSpot.update({ - where: { id }, - data: { + const [updated] = await db + .update(placementSpot) + .set({ ...updateData, parentSpotId: updateData.parentSpotId || null, - }, - }) + }) + .where(eq(placementSpot.id, id)) + .returning({ id: placementSpot.id }) + + // Updating a missing row used to throw (P2025); keep that error path. + if (!updated) { + throw new Error(ERROR_MESSAGES.SPOT_UPDATE_ERROR) + } } catch (error) { logger.errorWithCause('Failed to update spot', error, { spotId: id, housingUnitId }) throw new Error(ERROR_MESSAGES.SPOT_UPDATE_ERROR) @@ -83,22 +87,27 @@ export async function deleteSpot(formData: FormData): Promise { try { // Check if spot has active placements - const activePlacements = await prisma.placement.count({ - where: { spotId: id, status: 'ACTIVE' }, - }) + const activePlacements = await db.$count( + placement, + and(eq(placement.spotId, id), eq(placement.status, 'ACTIVE')), + ) if (activePlacements > 0) { throw new Error(ERROR_MESSAGES.SPOT_DELETE_BLOCKED) } // Delete child spots first if this is a container - await prisma.placementSpot.deleteMany({ - where: { parentSpotId: id }, - }) + await db.delete(placementSpot).where(eq(placementSpot.parentSpotId, id)) - await prisma.placementSpot.delete({ - where: { id }, - }) + const deleted = await db + .delete(placementSpot) + .where(eq(placementSpot.id, id)) + .returning({ id: placementSpot.id }) + + // Deleting a missing row used to throw (P2025); keep that error path. + if (deleted.length === 0) { + throw new Error(ERROR_MESSAGES.SPOT_DELETE_ERROR) + } } catch (error) { if (error instanceof Error && error.message.includes(ERROR_MESSAGES.SPOT_DELETE_BLOCKED)) { throw error @@ -118,8 +127,9 @@ export async function createMultipleSpots(formData: FormData): Promise { try { // Create the room (container) - const room = await prisma.placementSpot.create({ - data: { + const [room] = await db + .insert(placementSpot) + .values({ housingUnitId: data.housingUnitId, code: data.roomCode, label: data.roomLabel, @@ -127,24 +137,22 @@ export async function createMultipleSpots(formData: FormData): Promise { squareMeters: data.squareMeters, floor: data.floor, status: DEFAULT_STATUSES.spot, - }, - }) + }) + .returning() // Create beds inside the room for (let i = 1; i <= data.bedCount; i++) { - await prisma.placementSpot.create({ - data: { - housingUnitId: data.housingUnitId, - code: `${data.roomCode}-B${i}`, - label: `Bett ${i}`, - type: 'BED', - parentSpotId: room.id, - status: DEFAULT_STATUSES.spot, - }, + await db.insert(placementSpot).values({ + housingUnitId: data.housingUnitId, + code: `${data.roomCode}-B${i}`, + label: `Bett ${i}`, + type: 'BED', + parentSpotId: room.id, + status: DEFAULT_STATUSES.spot, }) } } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { throw new Error(ERROR_MESSAGES.SPOT_CODE_EXISTS) } logger.errorWithCause('Failed to create multiple spots', error, { diff --git a/src/lib/actions/transfers.ts b/src/lib/actions/transfers.ts index ff44de53..240f7dfd 100644 --- a/src/lib/actions/transfers.ts +++ b/src/lib/actions/transfers.ts @@ -1,6 +1,7 @@ 'use server' -import { prisma } from '@/lib/db' +import { db, transferRequest } from '@/lib/db' +import { and, desc, eq } from 'drizzle-orm' import { revalidatePath } from 'next/cache' import { logAudit } from '@/lib/audit' import { logger } from '@/lib/logger' @@ -12,22 +13,25 @@ import { RESIDENT_NAME_SELECT } from '@/lib/utils/resident-name' export async function getTransferRequests(status?: string) { await requireStaffAuth() const where = status - ? { status: status as 'PENDING' | 'APPROVED' | 'DENIED' | 'COMPLETED' | 'CANCELLED' } - : {} + ? eq( + transferRequest.status, + status as 'PENDING' | 'APPROVED' | 'DENIED' | 'COMPLETED' | 'CANCELLED', + ) + : undefined - return prisma.transferRequest.findMany({ + return db.query.transferRequest.findMany({ where, - include: { - resident: { select: { ...RESIDENT_NAME_SELECT, supportLevel: true } }, + with: { + resident: { columns: { ...RESIDENT_NAME_SELECT, supportLevel: true } }, currentPlacement: { - select: { - id: true, - housingUnit: { select: { id: true, code: true, address: true } }, + columns: { id: true }, + with: { + housingUnit: { columns: { id: true, code: true, address: true } }, }, }, - targetUnit: { select: { id: true, code: true, address: true } }, + targetUnit: { columns: { id: true, code: true, address: true } }, }, - orderBy: { createdAt: 'desc' }, + orderBy: [desc(transferRequest.createdAt)], }) } @@ -40,20 +44,21 @@ async function reviewTransferRequest( try { // Atomic guard: only PENDING rows are eligible. Prevents two staff members // concurrently approving + denying (or both approving) the same request. - const result = await prisma.transferRequest.updateMany({ - where: { id: input.requestId, status: 'PENDING' }, - data: { + const updated = await db + .update(transferRequest) + .set({ status: decision, staffNotes: input.staffNotes, reviewedBy: user.id, reviewedAt: new Date(), - }, - }) + }) + .where(and(eq(transferRequest.id, input.requestId), eq(transferRequest.status, 'PENDING'))) + .returning({ id: transferRequest.id }) - if (result.count === 0) { - const exists = await prisma.transferRequest.findUnique({ - where: { id: input.requestId }, - select: { id: true }, + if (updated.length === 0) { + const exists = await db.query.transferRequest.findFirst({ + where: eq(transferRequest.id, input.requestId), + columns: { id: true }, }) return { success: false, diff --git a/src/lib/ai/staff-chat-tools.ts b/src/lib/ai/staff-chat-tools.ts index fb771bd4..8596f58b 100644 --- a/src/lib/ai/staff-chat-tools.ts +++ b/src/lib/ai/staff-chat-tools.ts @@ -1,5 +1,15 @@ import { z } from 'zod' -import { prisma } from '@/lib/db' +import { and, asc, desc, eq, inArray, isNull, like, type SQL } from 'drizzle-orm' +import { + db, + escapeLike, + housingUnit, + incident, + maintenanceRequest, + placement, + resident, + transferRequest, +} from '@/lib/db' /** Server-side cap on per-tool result size, regardless of what the LLM asks for. */ export const MAX_TOOL_LIMIT = 25 @@ -97,11 +107,11 @@ export async function executeStaffChatTool(name: string, rawInput: unknown): Pro case 'get_dashboard_stats': { const [totalResidents, activePlacements, totalUnits, openIncidents, pendingTransfers] = await Promise.all([ - prisma.resident.count(), - prisma.placement.count({ where: { status: 'ACTIVE' } }), - prisma.housingUnit.count(), - prisma.incident.count({ where: { resolvedAt: null } }), - prisma.transferRequest.count({ where: { status: 'PENDING' } }), + db.$count(resident), + db.$count(placement, eq(placement.status, 'ACTIVE')), + db.$count(housingUnit), + db.$count(incident, isNull(incident.resolvedAt)), + db.$count(transferRequest, eq(transferRequest.status, 'PENDING')), ]) return { totalResidents, @@ -119,29 +129,33 @@ export async function executeStaffChatTool(name: string, rawInput: unknown): Pro if (!parsed.success) return { error: 'Ungültige Eingabe' } const input = parsed.data - const where: Record = {} - if (input.code) where.code = { contains: input.code.toUpperCase() } - if (input.status) where.status = input.status + const conditions: SQL[] = [] + if (input.code) + conditions.push(like(resident.code, `%${escapeLike(input.code.toUpperCase())}%`)) + if (input.status) conditions.push(eq(resident.status, input.status)) - const residents = await prisma.resident.findMany({ - where, - select: { + const residents = await db.query.resident.findMany({ + where: and(...conditions), + columns: { code: true, status: true, languages: true, + }, + with: { placements: { - where: { status: 'ACTIVE' }, - select: { housingUnit: { select: { code: true, address: true } } }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + columns: {}, + with: { housingUnit: { columns: { code: true, address: true } } }, + limit: 1, }, }, - take: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), - orderBy: { createdAt: 'desc' }, + limit: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), + orderBy: [desc(resident.createdAt)], }) return residents.map((r) => ({ code: r.code, status: r.status, - languages: r.languages, + languages: r.languages ?? [], currentUnit: r.placements[0]?.housingUnit?.code ?? null, unitAddress: r.placements[0]?.housingUnit?.address ?? null, })) @@ -152,20 +166,22 @@ export async function executeStaffChatTool(name: string, rawInput: unknown): Pro if (!parsed.success) return { error: 'Ungültige Eingabe' } const input = parsed.data - const units = await prisma.housingUnit.findMany({ - select: { + const units = await db.query.housingUnit.findMany({ + columns: { code: true, address: true, totalBeds: true, - placements: { where: { status: 'ACTIVE' }, select: { id: true } }, - incidents: { where: { resolvedAt: null }, select: { id: true } }, + }, + with: { + placements: { where: eq(placement.status, 'ACTIVE'), columns: { id: true } }, + incidents: { where: isNull(incident.resolvedAt), columns: { id: true } }, maintenanceRequests: { - where: { status: { in: ['OPEN', 'IN_PROGRESS'] } }, - select: { id: true }, + where: inArray(maintenanceRequest.status, ['OPEN', 'IN_PROGRESS']), + columns: { id: true }, }, }, - take: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), - orderBy: { code: 'asc' }, + limit: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), + orderBy: [asc(housingUnit.code)], }) const result = units.map((u) => ({ code: u.code, @@ -184,22 +200,24 @@ export async function executeStaffChatTool(name: string, rawInput: unknown): Pro if (!parsed.success) return { error: 'Ungültige Eingabe' } const input = parsed.data - const where: Record = {} - if (input.category) where.category = input.category - if (input.unresolved) where.resolvedAt = null + const conditions: SQL[] = [] + if (input.category) conditions.push(eq(incident.category, input.category)) + if (input.unresolved) conditions.push(isNull(incident.resolvedAt)) - const incidents = await prisma.incident.findMany({ - where, - select: { + const incidents = await db.query.incident.findMany({ + where: and(...conditions), + columns: { type: true, category: true, severity: true, date: true, resolvedAt: true, - housingUnit: { select: { code: true } }, }, - orderBy: { date: 'desc' }, - take: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), + with: { + housingUnit: { columns: { code: true } }, + }, + orderBy: [desc(incident.date)], + limit: Math.min(input.limit ?? 10, MAX_TOOL_LIMIT), }) return incidents.map((i) => ({ type: i.type, diff --git a/src/lib/analytics/algorithm-accuracy.ts b/src/lib/analytics/algorithm-accuracy.ts index 41342854..eb2a1ddf 100644 --- a/src/lib/analytics/algorithm-accuracy.ts +++ b/src/lib/analytics/algorithm-accuracy.ts @@ -7,7 +7,8 @@ * Answers: "Does higher compatibility actually lead to better outcomes?" */ -import { prisma } from '@/lib/db' +import { eq, ne } from 'drizzle-orm' +import { db, incident, placement } from '@/lib/db' import { SCORE_THRESHOLDS } from '@/lib/config/thresholds' // ───────────────────────────────────────────────────────────────────────────── @@ -75,11 +76,9 @@ const TIERS = [ */ export async function calculateAlgorithmAccuracy(): Promise { // Fetch all ended placements with their scores and check-ins - const endedPlacements = await prisma.placement.findMany({ - where: { - status: { not: 'ACTIVE' }, - }, - select: { + const endedPlacements = await db.query.placement.findMany({ + where: ne(placement.status, 'ACTIVE'), + columns: { id: true, compatibilityScore: true, startDate: true, @@ -88,8 +87,10 @@ export async function calculateAlgorithmAccuracy(): Promise i.predictable === true).length diff --git a/src/lib/analytics/mission-kpis.ts b/src/lib/analytics/mission-kpis.ts index bfc896e8..764c31f2 100644 --- a/src/lib/analytics/mission-kpis.ts +++ b/src/lib/analytics/mission-kpis.ts @@ -10,7 +10,8 @@ * @see CLAUDE.md "Measuring Success" section */ -import { prisma } from '@/lib/db' +import { and, asc, eq, gte, inArray, ne } from 'drizzle-orm' +import { db, incident, placement, resident } from '@/lib/db' import { zurichMonthKey, getZurichParts } from '@/lib/utils' // ───────────────────────────────────────────────────────────────────────────── @@ -101,40 +102,32 @@ export async function calculateMissionKPIs(months: number = 6): Promise const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000) // Fetch unit with related data - const unit = await prisma.housingUnit.findUnique({ - where: { id: unitId }, - include: { + const unit = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.id, unitId), + with: { placements: { - where: { - startDate: { gte: sixMonthsAgo }, - }, - include: { + where: gte(placement.startDate, sixMonthsAgo), + with: { checkIns: { - select: { + columns: { overallSatisfaction: true, }, }, }, }, incidents: { - where: { - category: 'INTERPERSONAL', - date: { gte: sixMonthsAgo }, - }, + where: and(eq(incident.category, 'INTERPERSONAL'), gte(incident.date, sixMonthsAgo)), }, }, }) @@ -79,12 +75,10 @@ export async function calculateUnitMetrics(unitId: string): Promise const recentConflicts = unit.incidents.filter((i) => new Date(i.date) >= thirtyDaysAgo).length // Total conflicts (all incidents) - const totalConflicts = await prisma.incident.count({ - where: { - housingUnitId: unitId, - category: 'INTERPERSONAL', - }, - }) + const totalConflicts = await db.$count( + incident, + and(eq(incident.housingUnitId, unitId), eq(incident.category, 'INTERPERSONAL')), + ) // Placement duration analysis const endedPlacements = unit.placements.filter((p) => p.endDate !== null) @@ -115,12 +109,10 @@ export async function calculateUnitMetrics(unitId: string): Promise endedPlacements.length > 0 ? (conflictEnds.length / endedPlacements.length) * 100 : 0 // Current occupancy - const activePlacements = await prisma.placement.count({ - where: { - housingUnitId: unitId, - status: 'ACTIVE', - }, - }) + const activePlacements = await db.$count( + placement, + and(eq(placement.housingUnitId, unitId), eq(placement.status, 'ACTIVE')), + ) const occupancyRate = (activePlacements / unit.totalBeds) * 100 // Average satisfaction from check-ins @@ -134,13 +126,13 @@ export async function calculateUnitMetrics(unitId: string): Promise // Was 12 sequential counts per call (worst-case N+1 in matching page). const monthsToCheck = 12 const monthsWindowStart = new Date(now.getFullYear(), now.getMonth() - monthsToCheck, 1) - const interpersonalIncidents = await prisma.incident.findMany({ - where: { - housingUnitId: unitId, - category: 'INTERPERSONAL', - date: { gte: monthsWindowStart }, - }, - select: { date: true }, + const interpersonalIncidents = await db.query.incident.findMany({ + where: and( + eq(incident.housingUnitId, unitId), + eq(incident.category, 'INTERPERSONAL'), + gte(incident.date, monthsWindowStart), + ), + columns: { date: true }, }) const monthsWithIncidents = new Set(interpersonalIncidents.map((i) => zurichMonthKey(i.date))) @@ -208,9 +200,9 @@ export async function calculateUnitMetrics(unitId: string): Promise * Calculate metrics for all units (for dashboard) */ export async function calculateAllUnitMetrics(): Promise { - const units = await prisma.housingUnit.findMany({ - where: { status: { in: ['AVAILABLE', 'FULL'] } }, - select: { id: true }, + const units = await db.query.housingUnit.findMany({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + columns: { id: true }, }) const metrics = await Promise.all(units.map((u) => calculateUnitMetrics(u.id))) @@ -229,14 +221,12 @@ export async function getSimilarPlacementSuccessRate( totalPlacements: number successfulPlacements: number }> { - const placements = await prisma.placement.findMany({ - where: { - compatibilityScore: { - gte: compatibilityScore - range, - lte: compatibilityScore + range, - }, - endDate: { not: null }, - }, + const placements = await db.query.placement.findMany({ + where: and( + gte(placement.compatibilityScore, compatibilityScore - range), + lte(placement.compatibilityScore, compatibilityScore + range), + isNotNull(placement.endDate), + ), }) if (placements.length === 0) { diff --git a/src/lib/audit.ts b/src/lib/audit.ts index 027fd44d..431742de 100644 --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -5,8 +5,8 @@ * Auth-ready: Automatically captures current user when auth is implemented */ -import { prisma } from '@/lib/db' -import { Prisma } from '@prisma/client' +import { db, auditLog } from '@/lib/db' +import { and, eq, desc } from 'drizzle-orm' import { getCurrentUser } from '@/lib/auth' import { logger } from '@/lib/logger' import { QUERY_LIMITS } from '@/lib/config/thresholds' @@ -40,7 +40,8 @@ interface AuditLogEntry { entity: AuditEntity entityId: string userId?: string // Optional: will auto-capture from session if not provided - changes?: Prisma.InputJsonValue + // The jsonb column types as `unknown` — anything JSON-serialisable is fine. + changes?: unknown reason?: string } @@ -68,15 +69,13 @@ export async function logAudit({ resolvedUserId = currentUser?.id } - await prisma.auditLog.create({ - data: { - action, - entity, - entityId, - userId: resolvedUserId, - changes: changes ?? undefined, - reason, - }, + await db.insert(auditLog).values({ + action, + entity, + entityId, + userId: resolvedUserId, + changes: changes ?? undefined, + reason, }) } catch (error) { // Log error but don't throw - audit logging should never break operations @@ -89,10 +88,10 @@ export async function logAudit({ * Get audit history for an entity */ export async function getEntityAuditLog(entity: AuditEntity, entityId: string) { - return prisma.auditLog.findMany({ - where: { entity, entityId }, - orderBy: { createdAt: 'desc' }, - take: QUERY_LIMITS.entityHistory, + return db.query.auditLog.findMany({ + where: and(eq(auditLog.entity, entity), eq(auditLog.entityId, entityId)), + orderBy: [desc(auditLog.createdAt)], + limit: QUERY_LIMITS.entityHistory, }) } @@ -100,8 +99,8 @@ export async function getEntityAuditLog(entity: AuditEntity, entityId: string) { * Get recent audit logs (for admin dashboard) */ export async function getRecentAuditLogs(limit = 100) { - return prisma.auditLog.findMany({ - orderBy: { createdAt: 'desc' }, - take: limit, + return db.query.auditLog.findMany({ + orderBy: [desc(auditLog.createdAt)], + limit, }) } diff --git a/src/lib/auth/account.ts b/src/lib/auth/account.ts index 471f9a65..debb0f17 100644 --- a/src/lib/auth/account.ts +++ b/src/lib/auth/account.ts @@ -19,7 +19,8 @@ import { ALL_CODE_PREFIXES } from '@/lib/config/brand' import { ALL_RESIDENT_CODE_PREFIXES } from '@/lib/auth/code-prefixes' -import { prisma } from '@/lib/db' +import { db, account, user, resident } from '@/lib/db' +import { eq, type SQL } from 'drizzle-orm' import { hashPassword, verifyPassword } from './passwords' import { createAuthToken, consumeAuthToken } from './tokens' import { sendEmail } from '@/lib/email/service' @@ -53,11 +54,11 @@ export type AccountResult = { success: true; identities: AccountIdentities } | { success: false; error: string } const ACCOUNT_WITH_IDENTITIES = { - id: true, - email: true, - passwordHash: true, - user: { select: { id: true, code: true, name: true, role: true, active: true } }, - resident: { select: { id: true, code: true } }, + columns: { id: true, email: true, passwordHash: true }, + with: { + user: { columns: { id: true, code: true, name: true, role: true, active: true } }, + resident: { columns: { id: true, code: true } }, + }, } as const type AccountRow = { @@ -68,6 +69,20 @@ type AccountRow = { resident: { id: string; code: string } | null } +/** + * One account with both identities attached. + * + * The cast exists because schema.ts currently has circular table initializer + * references (Placement ↔ Incident et al.), which make `placement` implicitly + * `any` and poison drizzle's relational result types repo-wide. The runtime + * shape of this query is exactly AccountRow; drop the cast once the schema + * circularity is fixed. + */ +async function findAccountWithIdentities(where: SQL): Promise { + return (await db.query.account.findFirst({ where, ...ACCOUNT_WITH_IDENTITIES })) as + AccountRow | undefined +} + /** Shape an account row into the identities a session should carry. */ function toIdentities(account: AccountRow): AccountIdentities { const identities: AccountIdentities = {} @@ -94,17 +109,20 @@ type CodeIdentity = /** Resolve a login code to the identity it names. */ async function findIdentityByCode(code: string): Promise { if (ALL_CODE_PREFIXES.some((prefix) => code.startsWith(prefix))) { - const user = await prisma.user.findUnique({ - where: { code }, - select: { id: true, active: true }, + const staff = await db.query.user.findFirst({ + where: eq(user.code, code), + columns: { id: true, active: true }, }) - return user ? { kind: 'staff', id: user.id, active: user.active } : null + return staff ? { kind: 'staff', id: staff.id, active: staff.active } : null } // All prefixes, never just the active brand's — see loginByCode. if (ALL_RESIDENT_CODE_PREFIXES.some((prefix) => code.startsWith(prefix))) { - const resident = await prisma.resident.findUnique({ where: { code }, select: { id: true } }) - return resident ? { kind: 'resident', id: resident.id } : null + const row = await db.query.resident.findFirst({ + where: eq(resident.code, code), + columns: { id: true }, + }) + return row ? { kind: 'resident', id: row.id } : null } return null @@ -129,7 +147,10 @@ export async function sendVerificationEmail(accountId: string, email: string): P } async function loadAccount(where: { id: string }): Promise { - return prisma.account.findUniqueOrThrow({ where, select: ACCOUNT_WITH_IDENTITIES }) + const row = await findAccountWithIdentities(eq(account.id, where.id)) + // findUniqueOrThrow semantics: the caller just wrote this row, so a miss is a bug. + if (!row) throw new Error(`Account ${where.id} not found`) + return row } /** @@ -155,10 +176,14 @@ export async function registerAccount(input: { const identityLink = identity.kind === 'staff' ? { userId: identity.id } : { residentId: identity.id } + const identityWhere = + identity.kind === 'staff' + ? eq(account.userId, identity.id) + : eq(account.residentId, identity.id) const [accountForIdentity, accountForEmail] = await Promise.all([ - prisma.account.findFirst({ where: identityLink, select: ACCOUNT_WITH_IDENTITIES }), - prisma.account.findUnique({ where: { email }, select: ACCOUNT_WITH_IDENTITIES }), + findAccountWithIdentities(identityWhere), + findAccountWithIdentities(eq(account.email, email)), ]) // This code already belongs to a finished account — recover it, don't re-claim. @@ -180,13 +205,13 @@ export async function registerAccount(input: { if (!(await verifyPassword(password, accountForEmail.passwordHash))) { return { success: false, error: ERROR_MESSAGES.AUTH_LINK_PASSWORD_MISMATCH } } - await prisma.account.update({ where: { id: accountForEmail.id }, data: identityLink }) + await db.update(account).set(identityLink).where(eq(account.id, accountForEmail.id)) } else { // Known email, no password yet (invited, or migrated from a legacy row). - await prisma.account.update({ - where: { id: accountForEmail.id }, - data: { ...identityLink, passwordHash: await hashPassword(password) }, - }) + await db + .update(account) + .set({ ...identityLink, passwordHash: await hashPassword(password) }) + .where(eq(account.id, accountForEmail.id)) await sendVerificationEmail(accountForEmail.id, email) } @@ -199,18 +224,18 @@ export async function registerAccount(input: { // --- Finish this identity's own unclaimed account, or create one --- const accountId = accountForIdentity ? ( - await prisma.account.update({ - where: { id: accountForIdentity.id }, - data: { email, passwordHash: await hashPassword(password) }, - select: { id: true }, - }) - ).id + await db + .update(account) + .set({ email, passwordHash: await hashPassword(password) }) + .where(eq(account.id, accountForIdentity.id)) + .returning({ id: account.id }) + )[0].id : ( - await prisma.account.create({ - data: { email, passwordHash: await hashPassword(password), ...identityLink }, - select: { id: true }, - }) - ).id + await db + .insert(account) + .values({ email, passwordHash: await hashPassword(password), ...identityLink }) + .returning({ id: account.id }) + )[0].id await sendVerificationEmail(accountId, email) @@ -231,21 +256,15 @@ export async function loginWithEmail(input: { }): Promise { const failure = { success: false as const, error: ERROR_MESSAGES.INVALID_CREDENTIALS } - const account = await prisma.account.findUnique({ - where: { email: input.email }, - select: ACCOUNT_WITH_IDENTITIES, - }) - if (!account?.passwordHash) return failure - if (!(await verifyPassword(input.password, account.passwordHash))) return failure + const acct = await findAccountWithIdentities(eq(account.email, input.email)) + if (!acct?.passwordHash) return failure + if (!(await verifyPassword(input.password, acct.passwordHash))) return failure - const identities = toIdentities(account) + const identities = toIdentities(acct) if (!identities.staff && !identities.resident) return failure if (identities.staff) { - await prisma.user.update({ - where: { id: identities.staff.id }, - data: { lastLoginAt: new Date() }, - }) + await db.update(user).set({ lastLoginAt: new Date() }).where(eq(user.id, identities.staff.id)) } return { success: true, identities } @@ -263,9 +282,12 @@ export async function requestPasswordReset( return { success: false, error: ERROR_MESSAGES.AUTH_EMAIL_NOT_CONFIGURED } } - const account = await prisma.account.findUnique({ where: { email }, select: { id: true } }) - if (account) { - const raw = await createAuthToken(account.id, 'RESET_PASSWORD') + const acct = await db.query.account.findFirst({ + where: eq(account.email, email), + columns: { id: true }, + }) + if (acct) { + const raw = await createAuthToken(acct.id, 'RESET_PASSWORD') const { subject, html } = passwordResetEmail({ link: `${getAppUrl()}/reset-password?token=${raw}`, }) @@ -287,10 +309,10 @@ export async function resetPassword( const accountId = await consumeAuthToken(rawToken, 'RESET_PASSWORD') if (!accountId) return { success: false, error: ERROR_MESSAGES.AUTH_RESET_TOKEN_INVALID } - await prisma.account.update({ - where: { id: accountId }, - data: { passwordHash: await hashPassword(newPassword), emailVerifiedAt: new Date() }, - }) + await db + .update(account) + .set({ passwordHash: await hashPassword(newPassword), emailVerifiedAt: new Date() }) + .where(eq(account.id, accountId)) return { success: true } } @@ -300,9 +322,6 @@ export async function verifyEmailToken(rawToken: string): Promise { const accountId = await consumeAuthToken(rawToken, 'VERIFY_EMAIL') if (!accountId) return false - await prisma.account.update({ - where: { id: accountId }, - data: { emailVerifiedAt: new Date() }, - }) + await db.update(account).set({ emailVerifiedAt: new Date() }).where(eq(account.id, accountId)) return true } diff --git a/src/lib/auth/household.ts b/src/lib/auth/household.ts index 704fc983..5ba4428a 100644 --- a/src/lib/auth/household.ts +++ b/src/lib/auth/household.ts @@ -29,8 +29,16 @@ * Gated by `BRAND.features.selfServeHousehold`, which is OFF for AOZ. @see brand.ts */ -import { Prisma } from '@prisma/client' -import { prisma } from '@/lib/db' +import { + db, + account, + housingUnit, + placement, + resident, + isUniqueViolation, + type NewResident, +} from '@/lib/db' +import { eq } from 'drizzle-orm' import { BRAND } from '@/lib/config/brand' import { generateResidentCode } from './code-generation' import { hashPassword } from './passwords' @@ -72,7 +80,7 @@ function unansweredPreferences() { // frozen instance handed to every create is the kind of shared mutable // default that goes wrong quietly and late. languages: [] as string[], - } satisfies Prisma.ResidentCreateInput | Record + } satisfies Partial } /** @@ -123,15 +131,19 @@ export async function registerWithNewHousehold( // One email is one account, always — the unique index is the only thing // that could tell staff and resident logins apart, and it does not. - const existing = await prisma.account.findUnique({ where: { email }, select: { id: true } }) + const existing = await db.query.account.findFirst({ + where: eq(account.email, email), + columns: { id: true }, + }) if (existing) return { success: false, error: ERROR_MESSAGES.AUTH_EMAIL_TAKEN } const passwordHash = await hashPassword(password) try { - const created = await prisma.$transaction(async (tx) => { - const unit = await tx.housingUnit.create({ - data: { + const created = await db.transaction(async (tx) => { + const [unit] = await tx + .insert(housingUnit) + .values({ code: newHouseholdCode(), // The name the person typed IS the address line until they edit it. // A blank address on a required column would be a lie stored forever. @@ -141,34 +153,31 @@ export async function registerWithNewHousehold( // One bed, one occupant: this flat is at capacity the moment it is // created. AVAILABLE would advertise a free bed that does not exist. status: 'FULL', - }, - select: { id: true }, - }) + }) + .returning({ id: housingUnit.id }) - const resident = await tx.resident.create({ - data: { + const [founder] = await tx + .insert(resident) + .values({ code: generateResidentCode(), displayName: displayName?.trim() || null, status: 'PLACED', ...unansweredPreferences(), - }, - select: { id: true, code: true }, - }) + }) + .returning({ id: resident.id, code: resident.code }) - await tx.placement.create({ - data: { - residentId: resident.id, - housingUnitId: unit.id, - startDate: new Date(), - }, + await tx.insert(placement).values({ + residentId: founder.id, + housingUnitId: unit.id, + startDate: new Date(), }) - const account = await tx.account.create({ - data: { email, passwordHash, residentId: resident.id }, - select: { id: true }, - }) + const [acct] = await tx + .insert(account) + .values({ email, passwordHash, residentId: founder.id }) + .returning({ id: account.id }) - return { accountId: account.id, resident } + return { accountId: acct.id, resident: founder } }) // Outside the transaction AND swallowed on purpose. @@ -199,7 +208,7 @@ export async function registerWithNewHousehold( // The only realistic collision is a generated code that already exists. // Reporting it as a generic save failure is right: the caller retries, and // nothing about our code space is the user's business. - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { + if (isUniqueViolation(error)) { return { success: false, error: ERROR_MESSAGES.SAVE_ERROR } } throw error diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 3778a20f..da24217a 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -8,7 +8,8 @@ import { ALL_CODE_PREFIXES, BRAND } from '@/lib/config/brand' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, user, resident } from '@/lib/db' +import { eq } from 'drizzle-orm' import { AUTH_CONFIG } from './config' import { RESIDENT_COOKIE } from '@/lib/portal-auth' import { ALL_RESIDENT_CODE_PREFIXES, RESIDENT_CODE_PREFIX } from '@/lib/auth/code-prefixes' @@ -78,20 +79,22 @@ export async function getCurrentUser(): Promise { // A session must not outlive the account. The JWT alone would keep a // deactivated user (offboarded staff, retired demo account) signed in // until token expiry — with sliding refresh, indefinitely. - const user = await prisma.user.findUnique({ - where: { id: payload.sub }, - select: { + const row = await db.query.user.findFirst({ + where: eq(user.id, payload.sub), + columns: { active: true, scope: true, isSystemAdmin: true, siteAccess: true, - // Only the ids, and only when they can matter. An ALL_UNITS viewer — - // everyone, until somebody is deliberately narrowed — carries an empty - // list that nothing reads, so the common request does not grow a join. - unitAccess: { select: { housingUnitId: true } }, + }, + // Only the ids, and only when they can matter. An ALL_UNITS viewer — + // everyone, until somebody is deliberately narrowed — carries an empty + // list that nothing reads, so the common request does not grow a join. + with: { + unitAccess: { columns: { housingUnitId: true } }, }, }) - if (!user?.active) return null + if (!row?.active) return null return { id: payload.sub, @@ -102,17 +105,17 @@ export async function getCurrentUser(): Promise { // belongs in that same sentence: revoking someone's reach must take effect // on the next request, not at token expiry, which with sliding refresh is // indefinitely. - scope: user.scope, - isSystemAdmin: user.isSystemAdmin, - siteAccess: user.siteAccess, + scope: row.scope, + isSystemAdmin: row.isSystemAdmin, + siteAccess: row.siteAccess, // `?? []` guards a future caller that forgets to select the relation. // The column is NOT NULL with a default, so absence is impossible in // production — but this is the auth path, and an incomplete select should // narrow someone's reach, never throw and take the whole request down. assignedUnitIds: - user.siteAccess === 'ALL_UNITS' + row.siteAccess === 'ALL_UNITS' ? [] - : (user.unitAccess ?? []).map((row) => row.housingUnitId), + : (row.unitAccess ?? []).map((r) => r.housingUnitId), } } @@ -148,16 +151,16 @@ export async function getCurrentResident(): Promise { if (!residentCode) return null // Validate code exists in database - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true, code: true }, + const row = await db.query.resident.findFirst({ + where: eq(resident.code, residentCode), + columns: { id: true, code: true }, }) - if (!resident) return null + if (!row) return null return { - id: resident.id, - code: resident.code, + id: row.id, + code: row.code, } } @@ -262,9 +265,9 @@ export async function isAuthenticated(): Promise { export async function loginByCode(code: string, clientIp: string): Promise { if (ALL_CODE_PREFIXES.some((prefix) => code.startsWith(prefix))) { // Staff login - const user = await prisma.user.findUnique({ - where: { code }, - select: { + const staff = await db.query.user.findFirst({ + where: eq(user.code, code), + columns: { id: true, name: true, role: true, @@ -272,37 +275,34 @@ export async function loginByCode(code: string, clientIp: string): Promise code.startsWith(prefix))) { // Resident login - const resident = await prisma.resident.findUnique({ - where: { code }, - select: { id: true, code: true }, + const row = await db.query.resident.findFirst({ + where: eq(resident.code, code), + columns: { id: true, code: true }, }) - if (!resident) { + if (!row) { recordLoginAttempt(clientIp) return { success: false, error: 'Ungültiger Code' } } @@ -328,7 +328,7 @@ export async function loginByCode(code: string, clientIp: string): Promise = { // A reset link is a credential — keep its window short. @@ -34,14 +34,14 @@ export async function createAuthToken( ): Promise { const raw = randomBytes(32).toString('hex') - await prisma.authToken.deleteMany({ where: { accountId, purpose } }) - await prisma.authToken.create({ - data: { - accountId, - purpose, - tokenHash: hashAuthToken(raw), - expiresAt: new Date(Date.now() + TOKEN_TTL_MS[purpose]), - }, + await db + .delete(authToken) + .where(and(eq(authToken.accountId, accountId), eq(authToken.purpose, purpose))) + await db.insert(authToken).values({ + accountId, + purpose, + tokenHash: hashAuthToken(raw), + expiresAt: new Date(Date.now() + TOKEN_TTL_MS[purpose]), }) return raw @@ -56,15 +56,15 @@ export async function consumeAuthToken( raw: string, purpose: AuthTokenPurpose, ): Promise { - const token = await prisma.authToken.findUnique({ - where: { tokenHash: hashAuthToken(raw) }, - select: { id: true, purpose: true, expiresAt: true, usedAt: true, accountId: true }, + const token = await db.query.authToken.findFirst({ + where: eq(authToken.tokenHash, hashAuthToken(raw)), + columns: { id: true, purpose: true, expiresAt: true, usedAt: true, accountId: true }, }) if (!token || token.purpose !== purpose || token.usedAt || token.expiresAt < new Date()) { return null } - await prisma.authToken.update({ where: { id: token.id }, data: { usedAt: new Date() } }) + await db.update(authToken).set({ usedAt: new Date() }).where(eq(authToken.id, token.id)) return token.accountId } diff --git a/src/lib/chores/summary.ts b/src/lib/chores/summary.ts index e5c5d395..30f657d2 100644 --- a/src/lib/chores/summary.ts +++ b/src/lib/chores/summary.ts @@ -1,6 +1,6 @@ -import { prisma } from '@/lib/db' +import { db, householdTask, placement, resident, taskCompletion } from '@/lib/db' +import { and, asc, eq, gte } from 'drizzle-orm' import { zurichMonthKey } from '@/lib/utils' -import { RESIDENT_NAME_SELECT } from '@/lib/utils/resident-name' import { computeChoreBalances } from './balances' import type { ChoreBalanceRow } from '@/components/portal/ChoreBalanceSummary' @@ -27,37 +27,41 @@ export async function loadChoreBalances(housingUnitId: string): Promise zurichMonthKey(c.completedAt) === monthKey) - const residents = new Map(members.map((m) => [m.resident.id, m.resident])) + const residents = new Map(members.map((m) => [m.id, m])) const balances = computeChoreBalances( thisMonth.map((c) => ({ completedById: c.completedById, durationMinutes: c.durationMinutes, - taskEstimatedMinutes: c.task.estimatedMinutes, + taskEstimatedMinutes: c.taskEstimatedMinutes, })), - members.map((m) => m.resident.id), + members.map((m) => m.id), ) return balances.map((balance) => { diff --git a/src/lib/compatibility/convert.ts b/src/lib/compatibility/convert.ts index a41f5764..014d2f04 100644 --- a/src/lib/compatibility/convert.ts +++ b/src/lib/compatibility/convert.ts @@ -1,12 +1,12 @@ /** - * Convert Prisma Resident to ResidentProfile for scoring + * Convert a Resident row to ResidentProfile for scoring */ -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ResidentProfile } from './types' /** - * Convert a Prisma Resident record to a ResidentProfile for compatibility scoring + * Convert a Resident record to a ResidentProfile for compatibility scoring */ export function toResidentProfile(resident: Resident): ResidentProfile { return { @@ -22,11 +22,11 @@ export function toResidentProfile(resident: Resident): ResidentProfile { chaosTolerance: resident.chaosTolerance, guestTolerance: resident.guestTolerance, socialStyle: resident.socialStyle, - languages: resident.languages, + languages: resident.languages ?? [], culturalRegion: resident.culturalRegion ?? undefined, conflictStyle: resident.conflictStyle, smokingStatus: resident.smokingStatus, - dietaryNeeds: resident.dietaryNeeds, + dietaryNeeds: resident.dietaryNeeds ?? [], mobilityNeeds: resident.mobilityNeeds, medicalEquipment: resident.medicalEquipment, petTolerance: resident.petTolerance, diff --git a/src/lib/compatibility/placement-scores.ts b/src/lib/compatibility/placement-scores.ts index afddd19e..2f7bc68a 100644 --- a/src/lib/compatibility/placement-scores.ts +++ b/src/lib/compatibility/placement-scores.ts @@ -7,7 +7,7 @@ import { calculateCompatibility } from '@/lib/compatibility' import { toResidentProfile } from '@/lib/compatibility/convert' -import type { Resident, Placement } from '@prisma/client' +import type { Resident, Placement } from '@/lib/db' /** Calculate average compatibility scores between a resident and existing placements */ export function calculateAverageScores( diff --git a/src/lib/compatibility/room-fit.ts b/src/lib/compatibility/room-fit.ts index 259b44e9..0e1b13fb 100644 --- a/src/lib/compatibility/room-fit.ts +++ b/src/lib/compatibility/room-fit.ts @@ -8,7 +8,7 @@ * roommate was a blocking mismatch. */ -import type { PlacementSpot, Resident } from '@prisma/client' +import type { PlacementSpot, Resident } from '@/lib/db' import { calculateCompatibility } from './scoring' import { toResidentProfile } from './convert' diff --git a/src/lib/compatibility/save-assessment.ts b/src/lib/compatibility/save-assessment.ts index 5f0f6ad4..15c691b1 100644 --- a/src/lib/compatibility/save-assessment.ts +++ b/src/lib/compatibility/save-assessment.ts @@ -6,14 +6,15 @@ * a UNION. This helper makes both upserts atomic from the caller's perspective. */ -import type { Prisma } from '@prisma/client' +import { compatibilityAssessment } from '@/lib/db' +import type { db } from '@/lib/db' import type { CompatibilityScore } from './types' -type CompatibilityAssessmentTx = { - compatibilityAssessment: { - upsert: (args: Prisma.CompatibilityAssessmentUpsertArgs) => Promise - } -} +/** + * Any Drizzle client that can run inserts — the `db` singleton or, as every + * current caller does, the `tx` handed out by `db.transaction()`. + */ +type CompatibilityAssessmentTx = Pick export async function saveBidirectionalAssessment( tx: CompatibilityAssessmentTx, @@ -31,25 +32,19 @@ export async function saveBidirectionalAssessment( concerns: score.concerns || [], } - await tx.compatibilityAssessment.upsert({ - where: { - residentId_comparedWithId: { - residentId: residentAId, - comparedWithId: residentBId, - }, - }, - update: data, - create: { residentId: residentAId, comparedWithId: residentBId, ...data }, - }) + await tx + .insert(compatibilityAssessment) + .values({ residentId: residentAId, comparedWithId: residentBId, ...data }) + .onConflictDoUpdate({ + target: [compatibilityAssessment.residentId, compatibilityAssessment.comparedWithId], + set: data, + }) - await tx.compatibilityAssessment.upsert({ - where: { - residentId_comparedWithId: { - residentId: residentBId, - comparedWithId: residentAId, - }, - }, - update: data, - create: { residentId: residentBId, comparedWithId: residentAId, ...data }, - }) + await tx + .insert(compatibilityAssessment) + .values({ residentId: residentBId, comparedWithId: residentAId, ...data }) + .onConflictDoUpdate({ + target: [compatibilityAssessment.residentId, compatibilityAssessment.comparedWithId], + set: data, + }) } diff --git a/src/lib/compatibility/types.ts b/src/lib/compatibility/types.ts index f31ff946..2569141a 100644 --- a/src/lib/compatibility/types.ts +++ b/src/lib/compatibility/types.ts @@ -14,7 +14,7 @@ import type { SupportLevel, RecyclingKnowledge, ConflictStyle, -} from '@prisma/client' +} from '@/lib/db' import type { CleanlinessProfile } from './cleanliness' export type { @@ -79,7 +79,7 @@ export interface ResidentProfile { supportLevel: SupportLevel } -// Types imported from @prisma/client above (SSOT: schema.prisma) +// Types imported from @/lib/db above (SSOT: src/lib/db/schema.ts) export interface CompatibilityScore { overall: number // 0-100 diff --git a/src/lib/config/conflict-resolution.ts b/src/lib/config/conflict-resolution.ts index d1b2fd71..c6d33b5c 100644 --- a/src/lib/config/conflict-resolution.ts +++ b/src/lib/config/conflict-resolution.ts @@ -21,7 +21,7 @@ import type { IncidentSeverity, ResolutionStage, AgreementStatus, -} from '@prisma/client' +} from '@/lib/db' // ============================================================================= // THE LADDER diff --git a/src/lib/config/decisions.ts b/src/lib/config/decisions.ts index ee795321..2d0e1612 100644 --- a/src/lib/config/decisions.ts +++ b/src/lib/config/decisions.ts @@ -28,7 +28,7 @@ import type { RuleCategory, VoteChoice, VoteThreshold, -} from '@prisma/client' +} from '@/lib/db' // ============================================================================= // WHO DECIDES — per rule category diff --git a/src/lib/config/events.ts b/src/lib/config/events.ts index 02ad4e26..832ed585 100644 --- a/src/lib/config/events.ts +++ b/src/lib/config/events.ts @@ -12,7 +12,7 @@ * fails to compile until it has a word. */ -import type { EventRsvpStatus, HouseEventCategory } from '@prisma/client' +import type { EventRsvpStatus, HouseEventCategory } from '@/lib/db' import type { MessageKey } from '@/lib/i18n' export const HOUSE_EVENT_CATEGORY_LABEL_KEYS: Record = { diff --git a/src/lib/config/house-rules.ts b/src/lib/config/house-rules.ts index 61d0f7ee..79373bca 100644 --- a/src/lib/config/house-rules.ts +++ b/src/lib/config/house-rules.ts @@ -16,7 +16,7 @@ */ import { BRAND } from './brand' -import type { RuleCategory, RuleDelegation, RuleScope, RuleStatus } from '@prisma/client' +import type { RuleCategory, RuleDelegation, RuleScope, RuleStatus } from '@/lib/db' // ============================================================================= // SCOPE diff --git a/src/lib/config/household-tasks.ts b/src/lib/config/household-tasks.ts index fa15e730..d3ec733c 100644 --- a/src/lib/config/household-tasks.ts +++ b/src/lib/config/household-tasks.ts @@ -184,7 +184,7 @@ export const TASK_TEMPLATES: TaskTemplate[] = [ // COMPLAINT → INCIDENT CATEGORY MAPPING // ============================================================================= -import type { HouseholdTaskCategory, IncidentType } from '@prisma/client' +import type { HouseholdTaskCategory, IncidentType } from '@/lib/db' export const CHORE_COMPLAINT_INCIDENT_MAP: Record = { CLEANING: 'CLEANLINESS_DISPUTE', diff --git a/src/lib/config/marketplace.ts b/src/lib/config/marketplace.ts index bb03924e..95a1f970 100644 --- a/src/lib/config/marketplace.ts +++ b/src/lib/config/marketplace.ts @@ -31,7 +31,7 @@ * a value the code cannot handle. */ -import type { MarketplacePostKind, MarketplacePostStatus } from '@prisma/client' +import type { MarketplacePostKind, MarketplacePostStatus } from '@/lib/db' import type { MessageKey } from '@/lib/i18n' /** The two halves of the board. */ diff --git a/src/lib/constants/labels/complaints.ts b/src/lib/constants/labels/complaints.ts index 24d3a38b..af71ccdd 100644 --- a/src/lib/constants/labels/complaints.ts +++ b/src/lib/constants/labels/complaints.ts @@ -1,4 +1,4 @@ -import type { ComplaintStatus, ComplaintSubject } from '@prisma/client' +import type { ComplaintStatus, ComplaintSubject } from '@/lib/db' /** * Complaints about the organisation — resident-facing and staff-facing German. diff --git a/src/lib/constants/labels/events.ts b/src/lib/constants/labels/events.ts index 4fe78e9a..34271ded 100644 --- a/src/lib/constants/labels/events.ts +++ b/src/lib/constants/labels/events.ts @@ -13,7 +13,7 @@ import { de } from '@/lib/i18n/dictionaries/de' import { HOUSE_EVENT_CATEGORIES, HOUSE_EVENT_CATEGORY_LABEL_KEYS } from '@/lib/config/events' -import type { HouseEventCategory } from '@prisma/client' +import type { HouseEventCategory } from '@/lib/db' const categoryLabels = Object.fromEntries( HOUSE_EVENT_CATEGORIES.map((category) => [ diff --git a/src/lib/constants/labels/marketplace.ts b/src/lib/constants/labels/marketplace.ts index a7f35eb7..7a63428d 100644 --- a/src/lib/constants/labels/marketplace.ts +++ b/src/lib/constants/labels/marketplace.ts @@ -22,7 +22,7 @@ import { natureOfKind, type MarketplaceNature, } from '@/lib/config/marketplace' -import type { MarketplacePostKind } from '@prisma/client' +import type { MarketplacePostKind } from '@/lib/db' const kindLabels = Object.fromEntries( MARKETPLACE_KIND_VALUES.map((kind) => [kind, de[MARKETPLACE_KINDS[kind].labelKey]]), diff --git a/src/lib/data/activities.ts b/src/lib/data/activities.ts index 5b4a2c71..0f0dbc8c 100644 --- a/src/lib/data/activities.ts +++ b/src/lib/data/activities.ts @@ -1,6 +1,6 @@ -import { Prisma } from '@prisma/client' +import { sql, type SQL } from 'drizzle-orm' import { randomBytes } from 'crypto' -import { prisma } from '@/lib/db' +import { db } from '@/lib/db' import type { ActivityCategory, ActivityRecord, ActivityStatus } from '@/lib/config/activities' type ActivityWriteData = { @@ -29,13 +29,14 @@ function createCuidLikeId() { } export async function getActivityById(id: string): Promise { - const rows = await prisma.$queryRaw` + const { rows } = await db.execute(sql` SELECT * FROM "Activity" WHERE "id" = ${id} LIMIT 1 - ` - return rows[0] ? mapActivity(rows[0]) : null + `) + const records = rows as unknown as ActivityRecord[] + return records[0] ? mapActivity(records[0]) : null } export async function listActivities( @@ -48,38 +49,37 @@ export async function listActivities( take?: number } = {}, ): Promise { - const where: Prisma.Sql[] = [] + const where: SQL[] = [] if (options.publishedOnly) { - where.push(Prisma.sql`"status" = 'PUBLISHED'::"ActivityStatus"`) + where.push(sql`"status" = 'PUBLISHED'::"ActivityStatus"`) } else if (options.status) { - where.push(Prisma.sql`"status" = ${options.status}::"ActivityStatus"`) + where.push(sql`"status" = ${options.status}::"ActivityStatus"`) } if (options.category) { - where.push(Prisma.sql`"category" = ${options.category}::"ActivityCategory"`) + where.push(sql`"category" = ${options.category}::"ActivityCategory"`) } if (options.highlightedOnly) { - where.push(Prisma.sql`"highlight" = true`) + where.push(sql`"highlight" = true`) } if (options.activeOn) { - where.push(Prisma.sql`("endsAt" IS NULL OR "endsAt" >= ${options.activeOn})`) + where.push(sql`("endsAt" IS NULL OR "endsAt" >= ${options.activeOn})`) } - const whereClause = - where.length > 0 ? Prisma.sql`WHERE ${Prisma.join(where, ' AND ')}` : Prisma.empty - const limitClause = - typeof options.take === 'number' ? Prisma.sql`LIMIT ${options.take}` : Prisma.empty + const whereClause = where.length > 0 ? sql`WHERE ${sql.join(where, sql` AND `)}` : sql`` + const limitClause = typeof options.take === 'number' ? sql`LIMIT ${options.take}` : sql`` - return prisma.$queryRaw` + const { rows } = await db.execute(sql` SELECT * FROM "Activity" ${whereClause} ORDER BY "status" ASC, "highlight" DESC, "startsAt" ASC NULLS LAST, "updatedAt" DESC ${limitClause} - ` + `) + return rows as unknown as ActivityRecord[] } export async function countActivities( @@ -88,25 +88,26 @@ export async function countActivities( highlightedPublished?: boolean } = {}, ): Promise { - const where: Prisma.Sql[] = [] + const where: SQL[] = [] if (options.status) { - where.push(Prisma.sql`"status" = ${options.status}::"ActivityStatus"`) + where.push(sql`"status" = ${options.status}::"ActivityStatus"`) } if (options.highlightedPublished) { - where.push(Prisma.sql`"status" = 'PUBLISHED'::"ActivityStatus" AND "highlight" = true`) + where.push(sql`"status" = 'PUBLISHED'::"ActivityStatus" AND "highlight" = true`) } - const whereClause = - where.length > 0 ? Prisma.sql`WHERE ${Prisma.join(where, ' AND ')}` : Prisma.empty - const rows = await prisma.$queryRaw>` + const whereClause = where.length > 0 ? sql`WHERE ${sql.join(where, sql` AND `)}` : sql`` + const { rows } = await db.execute(sql` SELECT COUNT(*)::bigint AS count FROM "Activity" ${whereClause} - ` - return Number(rows[0]?.count ?? 0) + `) + // COUNT arrives as a string through node-postgres — coerce before returning. + const records = rows as unknown as Array<{ count: string }> + return Number(records[0]?.count ?? 0) } export async function createActivityRecord(data: ActivityWriteData): Promise { - const rows = await prisma.$queryRaw` + const { rows } = await db.execute(sql` INSERT INTO "Activity" ( "id", "updatedAt", "title", "description", "category", "cost", "costNote", "location", "website", "phone", "schedule", "startsAt", "endsAt", @@ -119,15 +120,16 @@ export async function createActivityRecord(data: ActivityWriteData): Promise { - const rows = await prisma.$queryRaw` + const { rows } = await db.execute(sql` UPDATE "Activity" SET "updatedAt" = now(), @@ -147,8 +149,9 @@ export async function updateActivityRecord( "updatedByUserId" = ${data.userId} WHERE "id" = ${id} RETURNING * - ` - return rows[0] + `) + const records = rows as unknown as ActivityRecord[] + return records[0] } export async function setActivityStatus( @@ -156,7 +159,7 @@ export async function setActivityStatus( status: ActivityStatus, userId: string, ): Promise { - await prisma.$executeRaw` + await db.execute(sql` UPDATE "Activity" SET "status" = ${status}::"ActivityStatus", @@ -164,5 +167,5 @@ export async function setActivityStatus( "updatedByUserId" = ${userId}, "updatedAt" = now() WHERE "id" = ${id} - ` + `) } diff --git a/src/lib/data/opportunities.ts b/src/lib/data/opportunities.ts index 59faafe0..9c895bf8 100644 --- a/src/lib/data/opportunities.ts +++ b/src/lib/data/opportunities.ts @@ -3,7 +3,8 @@ * `lib/opportunities/pipeline.ts` so they can be tested without a database. */ -import { prisma } from '@/lib/db' +import { and, asc, desc, eq, ilike, inArray, notInArray, or, type SQL } from 'drizzle-orm' +import { db, escapeLike, opportunity, opportunityApplication, resident } from '@/lib/db' import { RESIDENT_NAME_SELECT } from '@/lib/utils/resident-name' import type { ApplicationStageId, @@ -14,9 +15,9 @@ import { isActiveStage, occupiesSeat, openSeats } from '@/lib/opportunities/pipe /** Rows that reach the UI carry `displayName`, never a bare code. */ const APPLICATION_INCLUDE = { - resident: { select: RESIDENT_NAME_SELECT }, - supportedBy: { select: { id: true, name: true } }, - learningRecord: { select: { id: true } }, + resident: { columns: RESIDENT_NAME_SELECT }, + supportedBy: { columns: { id: true, name: true } }, + learningRecord: { columns: { id: true } }, } as const export interface OpportunityListFilters { @@ -26,25 +27,27 @@ export interface OpportunityListFilters { publishedOnly?: boolean } -function listWhere(filters: OpportunityListFilters) { +function listWhere(filters: OpportunityListFilters): SQL | undefined { const query = filters.query?.trim() ?? '' - return { - ...(filters.publishedOnly - ? { status: 'PUBLISHED' as const } - : filters.status - ? { status: filters.status } - : {}), - ...(filters.kind ? { kind: filters.kind } : {}), - ...(query - ? { - OR: [ - { title: { contains: query, mode: 'insensitive' as const } }, - { organisation: { contains: query, mode: 'insensitive' as const } }, - { location: { contains: query, mode: 'insensitive' as const } }, - ], - } - : {}), + const conditions: SQL[] = [] + if (filters.publishedOnly) { + conditions.push(eq(opportunity.status, 'PUBLISHED')) + } else if (filters.status) { + conditions.push(eq(opportunity.status, filters.status)) } + if (filters.kind) { + conditions.push(eq(opportunity.kind, filters.kind)) + } + if (query) { + const pattern = `%${escapeLike(query)}%` + const textMatch = or( + ilike(opportunity.title, pattern), + ilike(opportunity.organisation, pattern), + ilike(opportunity.location, pattern), + ) + if (textMatch) conditions.push(textMatch) + } + return and(...conditions) } /** @@ -52,25 +55,26 @@ function listWhere(filters: OpportunityListFilters) { * seats without a second query per row. */ export async function listOpportunities(filters: OpportunityListFilters = {}) { - return prisma.opportunity.findMany({ + return db.query.opportunity.findMany({ where: listWhere(filters), - include: { - applications: { select: { id: true, stage: true } }, + with: { + applications: { columns: { id: true, stage: true } }, }, - orderBy: [{ status: 'asc' }, { startsAt: 'asc' }, { updatedAt: 'desc' }], + orderBy: [asc(opportunity.status), asc(opportunity.startsAt), desc(opportunity.updatedAt)], }) } export async function getOpportunityDetail(id: string) { - return prisma.opportunity.findUnique({ - where: { id }, - include: { + const row = await db.query.opportunity.findFirst({ + where: eq(opportunity.id, id), + with: { applications: { - include: APPLICATION_INCLUDE, - orderBy: [{ stageChangedAt: 'desc' }], + with: APPLICATION_INCLUDE, + orderBy: [desc(opportunityApplication.stageChangedAt)], }, }, }) + return row ?? null } /** @@ -82,13 +86,14 @@ export async function getOpportunityDetail(id: string) { */ export async function opportunityStats() { const [total, published, drafts, activePeople, openThreads] = await Promise.all([ - prisma.opportunity.count(), - prisma.opportunity.count({ where: { status: 'PUBLISHED' } }), - prisma.opportunity.count({ where: { status: 'DRAFT' } }), - prisma.opportunityApplication.count({ where: { stage: 'STARTED' } }), - prisma.opportunityApplication.count({ - where: { stage: { in: ['INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED'] } }, - }), + db.$count(opportunity), + db.$count(opportunity, eq(opportunity.status, 'PUBLISHED')), + db.$count(opportunity, eq(opportunity.status, 'DRAFT')), + db.$count(opportunityApplication, eq(opportunityApplication.stage, 'STARTED')), + db.$count( + opportunityApplication, + inArray(opportunityApplication.stage, ['INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED']), + ), ]) return { total, published, drafts, activePeople, openThreads } @@ -102,34 +107,39 @@ export async function opportunityStats() { * coach is trying to record something real. */ export async function residentsAvailableFor(opportunityId: string) { - const attached = await prisma.opportunityApplication.findMany({ - where: { opportunityId }, - select: { residentId: true }, + const attached = await db.query.opportunityApplication.findMany({ + where: eq(opportunityApplication.opportunityId, opportunityId), + columns: { residentId: true }, }) - return prisma.resident.findMany({ - where: { - status: 'ACTIVE', - id: { notIn: attached.map((row) => row.residentId) }, - }, - select: RESIDENT_NAME_SELECT, - orderBy: [{ displayName: 'asc' }, { code: 'asc' }], + const attachedIds = attached.map((row) => row.residentId) + + return db.query.resident.findMany({ + where: and( + eq(resident.status, 'ACTIVE'), + // `notInArray` with an empty list is invalid SQL; with nobody attached + // there is nothing to exclude. + ...(attachedIds.length ? [notInArray(resident.id, attachedIds)] : []), + ), + columns: RESIDENT_NAME_SELECT, + orderBy: [asc(resident.displayName), asc(resident.code)], }) } export async function getApplication(id: string) { - return prisma.opportunityApplication.findUnique({ - where: { id }, - include: { ...APPLICATION_INCLUDE, opportunity: true }, + const row = await db.query.opportunityApplication.findFirst({ + where: eq(opportunityApplication.id, id), + with: { ...APPLICATION_INCLUDE, opportunity: true }, }) + return row ?? null } /** Everything a resident is currently attached to — used by their dossier. */ export async function listApplicationsForResident(residentId: string) { - return prisma.opportunityApplication.findMany({ - where: { residentId }, - include: { opportunity: true }, - orderBy: [{ stageChangedAt: 'desc' }], + return db.query.opportunityApplication.findMany({ + where: eq(opportunityApplication.residentId, residentId), + with: { opportunity: true }, + orderBy: [desc(opportunityApplication.stageChangedAt)], }) } @@ -144,26 +154,26 @@ export async function listApplicationsForResident(residentId: string) { */ export async function residentOpportunityBoard(residentId: string) { const [mine, published] = await Promise.all([ - prisma.opportunityApplication.findMany({ - where: { residentId }, - include: { opportunity: true }, - orderBy: [{ stageChangedAt: 'desc' }], + db.query.opportunityApplication.findMany({ + where: eq(opportunityApplication.residentId, residentId), + with: { opportunity: true }, + orderBy: [desc(opportunityApplication.stageChangedAt)], }), - prisma.opportunity.findMany({ - where: { status: 'PUBLISHED' }, - include: { applications: { select: { stage: true } } }, - orderBy: [{ startsAt: 'asc' }, { updatedAt: 'desc' }], + db.query.opportunity.findMany({ + where: eq(opportunity.status, 'PUBLISHED'), + with: { applications: { columns: { stage: true } } }, + orderBy: [asc(opportunity.startsAt), desc(opportunity.updatedAt)], }), ]) const attached = new Set(mine.map((application) => application.opportunityId)) const open = published - .filter((opportunity) => !attached.has(opportunity.id)) - .map(({ applications, ...opportunity }) => ({ - ...opportunity, + .filter((opportunityRow) => !attached.has(opportunityRow.id)) + .map(({ applications, ...opportunityRow }) => ({ + ...opportunityRow, seatsLeft: openSeats( - opportunity, + opportunityRow, applications.map((a) => a.stage as ApplicationStageId), ), })) diff --git a/src/lib/db/helpers.ts b/src/lib/db/helpers.ts new file mode 100644 index 00000000..f6824972 --- /dev/null +++ b/src/lib/db/helpers.ts @@ -0,0 +1,26 @@ +/** + * Small cross-cutting helpers the Prisma client used to provide implicitly. + */ +import { DatabaseError } from 'pg' + +/** + * Postgres unique-constraint violation (SQLSTATE 23505) — what Prisma + * surfaced as `PrismaClientKnownRequestError` with code `P2002`. + */ +export function isUniqueViolation(error: unknown): boolean { + if (error instanceof DatabaseError && error.code === '23505') return true + // drizzle-orm >= 0.44 wraps driver errors in DrizzleQueryError with the + // original pg error on `cause`. + if (error instanceof Error && error.cause !== undefined) { + return isUniqueViolation(error.cause) + } + return false +} + +/** + * Escape LIKE/ILIKE wildcards in user input so a search for "100%" matches + * the literal string. Prisma's `contains` escaped these internally. + */ +export function escapeLike(value: string): string { + return value.replace(/[\\%_]/g, (c) => `\\${c}`) +} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index e4466df2..bd1e24cb 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -30,3 +30,4 @@ export const db = new Proxy({} as DbInstance, { export * from './schema' export * from './types' +export * from './helpers' diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 22694cea..cb9f0458 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -21,1495 +21,2415 @@ import { createId } from '@paralleldrive/cuid2' import { sql } from 'drizzle-orm' -import { customType, pgTable, index, text, timestamp, doublePrecision, jsonb, integer, boolean, uniqueIndex, foreignKey, type AnyPgColumn, unique, check, pgEnum } from "drizzle-orm/pg-core" +import { + customType, + pgTable, + type PgTableExtraConfigValue, + index, + text, + timestamp, + doublePrecision, + jsonb, + integer, + boolean, + uniqueIndex, + foreignKey, + type AnyPgColumn, + unique, + check, + pgEnum, +} from 'drizzle-orm/pg-core' // Prisma's `Bytes` -> Postgres bytea. drizzle-orm has no built-in bytea type. const bytea = customType<{ data: Buffer; driverData: Buffer }>({ - dataType() { - return 'bytea' - }, + dataType() { + return 'bytea' + }, }) -export const activityCategory = pgEnum("ActivityCategory", ['SPORT', 'LANGUAGE', 'CULTURE', 'COMMUNITY', 'FAMILY', 'SUPPORT']) -export const activityCost = pgEnum("ActivityCost", ['FREE', 'REDUCED', 'PAID']) -export const activityStatus = pgEnum("ActivityStatus", ['DRAFT', 'PUBLISHED', 'ARCHIVED']) -export const ageRange = pgEnum("AgeRange", ['YOUNG_ADULT', 'ADULT', 'MIDDLE_AGED', 'SENIOR']) -export const agreementStatus = pgEnum("AgreementStatus", ['PROPOSED', 'ACCEPTED', 'HELD', 'BROKEN', 'EXPIRED']) -export const applicationStage = pgEnum("ApplicationStage", ['INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED', 'STARTED', 'ENDED', 'DECLINED']) -export const appointmentStatus = pgEnum("AppointmentStatus", ['SCHEDULED', 'COMPLETED', 'CANCELLED', 'NO_SHOW', 'REQUESTED']) -export const authTokenPurpose = pgEnum("AuthTokenPurpose", ['VERIFY_EMAIL', 'RESET_PASSWORD']) -export const careRole = pgEnum("CareRole", ['HOUSING', 'SOCIAL', 'JOB', 'VOLUNTEERING']) -export const checkInType = pgEnum("CheckInType", ['INITIAL', 'REGULAR', 'AD_HOC', 'EXIT']) -export const complaintStatus = pgEnum("ComplaintStatus", ['OPEN', 'IN_REVIEW', 'ANSWERED']) -export const complaintSubject = pgEnum("ComplaintSubject", ['STAFF', 'ACCOMMODATION', 'DECISION', 'OTHER']) -export const conflictStyle = pgEnum("ConflictStyle", ['AVOIDANT', 'COOPERATIVE', 'DIRECT']) -export const decisionMode = pgEnum("DecisionMode", ['RESIDENT_BINDING', 'RESIDENT_ADVISORY', 'STAFF_ONLY']) -export const endReason = pgEnum("EndReason", ['NATURAL', 'CONFLICT', 'REQUEST', 'CAPACITY', 'UPGRADE', 'OTHER']) -export const eventRsvpStatus = pgEnum("EventRsvpStatus", ['GOING', 'MAYBE', 'DECLINED']) -export const familyStatus = pgEnum("FamilyStatus", ['SINGLE', 'COUPLE', 'FAMILY_WITH_CHILDREN', 'SINGLE_PARENT']) -export const followUpPriority = pgEnum("FollowUpPriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) -export const gender = pgEnum("Gender", ['MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_SAY']) -export const houseEventCategory = pgEnum("HouseEventCategory", ['HOUSE_MEETING', 'SOCIAL', 'CULTURE', 'SUPPORT']) -export const houseEventStatus = pgEnum("HouseEventStatus", ['DRAFT', 'PUBLISHED', 'CANCELLED']) -export const householdTaskCategory = pgEnum("HouseholdTaskCategory", ['CLEANING', 'SHOPPING', 'MAINTENANCE', 'COOKING', 'TRASH', 'OTHER']) -export const householdTaskPriority = pgEnum("HouseholdTaskPriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) -export const householdTaskStatus = pgEnum("HouseholdTaskStatus", ['IDLE', 'NEEDS_ATTENTION', 'REQUESTED', 'IN_PROGRESS']) -export const householdTaskType = pgEnum("HouseholdTaskType", ['ONE_TIME', 'RECURRING_SCHEDULED', 'RECURRING_AS_NEEDED']) -export const housingStatus = pgEnum("HousingStatus", ['AVAILABLE', 'FULL', 'MAINTENANCE', 'CLOSED']) -export const incidentCategory = pgEnum("IncidentCategory", ['INTERPERSONAL', 'MAINTENANCE', 'SAFETY', 'WELLBEING']) -export const incidentSeverity = pgEnum("IncidentSeverity", ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']) -export const incidentType = pgEnum("IncidentType", ['NOISE_COMPLAINT', 'CLEANLINESS_DISPUTE', 'PERSONAL_CONFLICT', 'CULTURAL_FRICTION', 'SPACE_DISPUTE', 'SCHEDULE_CONFLICT', 'SAFETY_CONCERN', 'PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY_SYSTEM', 'GENERAL_MAINTENANCE', 'LOW_SATISFACTION', 'OTHER']) -export const involvementRole = pgEnum("InvolvementRole", ['INVOLVED', 'WITNESS', 'MEDIATOR']) -export const learningKind = pgEnum("LearningKind", ['LANGUAGE_TEST', 'COURSE', 'INFORMAL', 'QUALIFICATION', 'VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP']) -export const learningStatus = pgEnum("LearningStatus", ['PLANNED', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED']) -export const livingSkillsSupport = pgEnum("LivingSkillsSupport", ['INDEPENDENT', 'SOME_SUPPORT', 'REGULAR_SUPPORT']) -export const maintenanceCategory = pgEnum("MaintenanceCategory", ['PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY', 'CLEANING', 'EXTERIOR', 'OTHER']) -export const maintenancePriority = pgEnum("MaintenancePriority", ['LOW', 'NORMAL', 'HIGH', 'URGENT']) -export const maintenanceStatus = pgEnum("MaintenanceStatus", ['OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD', 'COMPLETED', 'CANCELLED']) -export const marketplacePostKind = pgEnum("MarketplacePostKind", ['GIVE_AWAY', 'LEND', 'WANTED', 'OFFER_HELP', 'NEED_HELP']) -export const marketplacePostStatus = pgEnum("MarketplacePostStatus", ['OPEN', 'CLAIMED', 'CLOSED']) -export const medicalDocType = pgEnum("MedicalDocType", ['PRIVATE_ROOM', 'STUDIO', 'BOTH']) -export const mobilityNeed = pgEnum("MobilityNeed", ['NONE', 'GROUND_FLOOR', 'WHEELCHAIR']) -export const opportunityKind = pgEnum("OpportunityKind", ['VOLUNTEERING', 'COMMUNITY_SERVICE', 'EMPLOYMENT', 'INTERNSHIP']) -export const opportunityStatus = pgEnum("OpportunityStatus", ['DRAFT', 'PUBLISHED', 'ARCHIVED']) -export const permitRequirement = pgEnum("PermitRequirement", ['NONE', 'EMPLOYER_NOTIFIES', 'PERMIT_REQUIRED']) -export const placementStatus = pgEnum("PlacementStatus", ['ACTIVE', 'ENDED', 'TRANSFERRED']) -export const profileVisibility = pgEnum("ProfileVisibility", ['PRIVATE', 'ROOMMATES', 'RESIDENTS']) -export const proposalStatus = pgEnum("ProposalStatus", ['DISCUSSION', 'VOTING', 'NEEDS_STAFF_CONFIRMATION', 'ACCEPTED', 'REJECTED', 'WITHDRAWN', 'VETOED', 'EXPIRED']) -export const proposalType = pgEnum("ProposalType", ['ADD_RULE', 'AMEND_RULE', 'REPEAL_RULE', 'HOUSE_DECISION']) -export const recyclingKnowledge = pgEnum("RecyclingKnowledge", ['NONE', 'BASIC', 'GOOD']) -export const residentOrStaff = pgEnum("ResidentOrStaff", ['RESIDENT', 'STAFF']) -export const residentStatus = pgEnum("ResidentStatus", ['ACTIVE', 'PLACED', 'TRANSFERRED', 'EXITED']) -export const resolutionStage = pgEnum("ResolutionStage", ['REPORTED', 'SELF_RESOLUTION', 'PEER_MEDIATION', 'STAFF_MEDIATION', 'FORMAL_MEASURE', 'CLOSED']) -export const roomSharingStatus = pgEnum("RoomSharingStatus", ['CAN_SHARE', 'PREFERS_PRIVATE', 'NEEDS_PRIVATE']) -export const ruleCategory = pgEnum("RuleCategory", ['SAFETY', 'RESPECT', 'NOISE', 'CLEANLINESS', 'KITCHEN', 'BATHROOM', 'GUESTS', 'SHARED_SPACES', 'COSTS', 'COMMUNICATION', 'OTHER']) -export const ruleDelegation = pgEnum("RuleDelegation", ['FIXED', 'UNIT_MAY_STRENGTHEN', 'UNIT_DECIDES']) -export const ruleScope = pgEnum("RuleScope", ['ORG', 'UNIT']) -export const ruleStatus = pgEnum("RuleStatus", ['ACTIVE', 'SUPERSEDED', 'ARCHIVED']) -export const sleepSchedule = pgEnum("SleepSchedule", ['EARLY_BIRD', 'STANDARD', 'NIGHT_OWL', 'IRREGULAR']) -export const smokingStatus = pgEnum("SmokingStatus", ['NON_SMOKER', 'OUTDOOR_SMOKER', 'INDOOR_SMOKER']) -export const socialStyle = pgEnum("SocialStyle", ['INTROVERTED', 'MODERATE', 'EXTROVERTED']) -export const spotStatus = pgEnum("SpotStatus", ['AVAILABLE', 'OCCUPIED', 'MAINTENANCE', 'CLOSED']) -export const spotType = pgEnum("SpotType", ['BED', 'PRIVATE_ROOM', 'STUDIO', 'ROOM']) -export const staffDecision = pgEnum("StaffDecision", ['CONFIRMED', 'VETOED']) -export const staffRole = pgEnum("StaffRole", ['ADMIN', 'BETREUUNG', 'SOZIALARBEIT', 'JOBCOACH', 'FREIWILLIGENARBEIT']) -export const staffScope = pgEnum("StaffScope", ['OWN_DOMAIN', 'ALL_DOMAINS']) -export const supportLevel = pgEnum("SupportLevel", ['STANDARD', 'ELEVATED', 'INTENSIVE']) -export const taskRequestStatus = pgEnum("TaskRequestStatus", ['PENDING', 'ACCEPTED', 'DECLINED', 'COMPLETED']) -export const transferRequestStatus = pgEnum("TransferRequestStatus", ['PENDING', 'APPROVED', 'DENIED', 'COMPLETED', 'CANCELLED']) -export const voteChoice = pgEnum("VoteChoice", ['YES', 'NO', 'ABSTAIN', 'BLOCK']) -export const voteThreshold = pgEnum("VoteThreshold", ['CONSENSUS', 'SUPERMAJORITY', 'SIMPLE_MAJORITY']) +export const activityCategory = pgEnum('ActivityCategory', [ + 'SPORT', + 'LANGUAGE', + 'CULTURE', + 'COMMUNITY', + 'FAMILY', + 'SUPPORT', +]) +export const activityCost = pgEnum('ActivityCost', ['FREE', 'REDUCED', 'PAID']) +export const activityStatus = pgEnum('ActivityStatus', ['DRAFT', 'PUBLISHED', 'ARCHIVED']) +export const ageRange = pgEnum('AgeRange', ['YOUNG_ADULT', 'ADULT', 'MIDDLE_AGED', 'SENIOR']) +export const agreementStatus = pgEnum('AgreementStatus', [ + 'PROPOSED', + 'ACCEPTED', + 'HELD', + 'BROKEN', + 'EXPIRED', +]) +export const applicationStage = pgEnum('ApplicationStage', [ + 'INTERESTED', + 'APPLIED', + 'INTERVIEW', + 'ACCEPTED', + 'STARTED', + 'ENDED', + 'DECLINED', +]) +export const appointmentStatus = pgEnum('AppointmentStatus', [ + 'SCHEDULED', + 'COMPLETED', + 'CANCELLED', + 'NO_SHOW', + 'REQUESTED', +]) +export const authTokenPurpose = pgEnum('AuthTokenPurpose', ['VERIFY_EMAIL', 'RESET_PASSWORD']) +export const careRole = pgEnum('CareRole', ['HOUSING', 'SOCIAL', 'JOB', 'VOLUNTEERING']) +export const checkInType = pgEnum('CheckInType', ['INITIAL', 'REGULAR', 'AD_HOC', 'EXIT']) +export const complaintStatus = pgEnum('ComplaintStatus', ['OPEN', 'IN_REVIEW', 'ANSWERED']) +export const complaintSubject = pgEnum('ComplaintSubject', [ + 'STAFF', + 'ACCOMMODATION', + 'DECISION', + 'OTHER', +]) +export const conflictStyle = pgEnum('ConflictStyle', ['AVOIDANT', 'COOPERATIVE', 'DIRECT']) +export const decisionMode = pgEnum('DecisionMode', [ + 'RESIDENT_BINDING', + 'RESIDENT_ADVISORY', + 'STAFF_ONLY', +]) +export const endReason = pgEnum('EndReason', [ + 'NATURAL', + 'CONFLICT', + 'REQUEST', + 'CAPACITY', + 'UPGRADE', + 'OTHER', +]) +export const eventRsvpStatus = pgEnum('EventRsvpStatus', ['GOING', 'MAYBE', 'DECLINED']) +export const familyStatus = pgEnum('FamilyStatus', [ + 'SINGLE', + 'COUPLE', + 'FAMILY_WITH_CHILDREN', + 'SINGLE_PARENT', +]) +export const followUpPriority = pgEnum('FollowUpPriority', ['LOW', 'NORMAL', 'HIGH', 'URGENT']) +export const gender = pgEnum('Gender', ['MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_SAY']) +export const houseEventCategory = pgEnum('HouseEventCategory', [ + 'HOUSE_MEETING', + 'SOCIAL', + 'CULTURE', + 'SUPPORT', +]) +export const houseEventStatus = pgEnum('HouseEventStatus', ['DRAFT', 'PUBLISHED', 'CANCELLED']) +export const householdTaskCategory = pgEnum('HouseholdTaskCategory', [ + 'CLEANING', + 'SHOPPING', + 'MAINTENANCE', + 'COOKING', + 'TRASH', + 'OTHER', +]) +export const householdTaskPriority = pgEnum('HouseholdTaskPriority', [ + 'LOW', + 'NORMAL', + 'HIGH', + 'URGENT', +]) +export const householdTaskStatus = pgEnum('HouseholdTaskStatus', [ + 'IDLE', + 'NEEDS_ATTENTION', + 'REQUESTED', + 'IN_PROGRESS', +]) +export const householdTaskType = pgEnum('HouseholdTaskType', [ + 'ONE_TIME', + 'RECURRING_SCHEDULED', + 'RECURRING_AS_NEEDED', +]) +export const housingStatus = pgEnum('HousingStatus', ['AVAILABLE', 'FULL', 'MAINTENANCE', 'CLOSED']) +export const incidentCategory = pgEnum('IncidentCategory', [ + 'INTERPERSONAL', + 'MAINTENANCE', + 'SAFETY', + 'WELLBEING', +]) +export const incidentSeverity = pgEnum('IncidentSeverity', ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']) +export const incidentType = pgEnum('IncidentType', [ + 'NOISE_COMPLAINT', + 'CLEANLINESS_DISPUTE', + 'PERSONAL_CONFLICT', + 'CULTURAL_FRICTION', + 'SPACE_DISPUTE', + 'SCHEDULE_CONFLICT', + 'SAFETY_CONCERN', + 'PLUMBING', + 'ELECTRICAL', + 'HEATING_COOLING', + 'APPLIANCE', + 'STRUCTURAL', + 'PEST_CONTROL', + 'SECURITY_SYSTEM', + 'GENERAL_MAINTENANCE', + 'LOW_SATISFACTION', + 'OTHER', +]) +export const involvementRole = pgEnum('InvolvementRole', ['INVOLVED', 'WITNESS', 'MEDIATOR']) +export const learningKind = pgEnum('LearningKind', [ + 'LANGUAGE_TEST', + 'COURSE', + 'INFORMAL', + 'QUALIFICATION', + 'VOLUNTEERING', + 'COMMUNITY_SERVICE', + 'EMPLOYMENT', + 'INTERNSHIP', +]) +export const learningStatus = pgEnum('LearningStatus', [ + 'PLANNED', + 'IN_PROGRESS', + 'COMPLETED', + 'EXPIRED', +]) +export const livingSkillsSupport = pgEnum('LivingSkillsSupport', [ + 'INDEPENDENT', + 'SOME_SUPPORT', + 'REGULAR_SUPPORT', +]) +export const maintenanceCategory = pgEnum('MaintenanceCategory', [ + 'PLUMBING', + 'ELECTRICAL', + 'HEATING_COOLING', + 'APPLIANCE', + 'STRUCTURAL', + 'PEST_CONTROL', + 'SECURITY', + 'CLEANING', + 'EXTERIOR', + 'OTHER', +]) +export const maintenancePriority = pgEnum('MaintenancePriority', [ + 'LOW', + 'NORMAL', + 'HIGH', + 'URGENT', +]) +export const maintenanceStatus = pgEnum('MaintenanceStatus', [ + 'OPEN', + 'ASSIGNED', + 'IN_PROGRESS', + 'ON_HOLD', + 'COMPLETED', + 'CANCELLED', +]) +export const marketplacePostKind = pgEnum('MarketplacePostKind', [ + 'GIVE_AWAY', + 'LEND', + 'WANTED', + 'OFFER_HELP', + 'NEED_HELP', +]) +export const marketplacePostStatus = pgEnum('MarketplacePostStatus', ['OPEN', 'CLAIMED', 'CLOSED']) +export const medicalDocType = pgEnum('MedicalDocType', ['PRIVATE_ROOM', 'STUDIO', 'BOTH']) +export const mobilityNeed = pgEnum('MobilityNeed', ['NONE', 'GROUND_FLOOR', 'WHEELCHAIR']) +export const opportunityKind = pgEnum('OpportunityKind', [ + 'VOLUNTEERING', + 'COMMUNITY_SERVICE', + 'EMPLOYMENT', + 'INTERNSHIP', +]) +export const opportunityStatus = pgEnum('OpportunityStatus', ['DRAFT', 'PUBLISHED', 'ARCHIVED']) +export const permitRequirement = pgEnum('PermitRequirement', [ + 'NONE', + 'EMPLOYER_NOTIFIES', + 'PERMIT_REQUIRED', +]) +export const placementStatus = pgEnum('PlacementStatus', ['ACTIVE', 'ENDED', 'TRANSFERRED']) +export const profileVisibility = pgEnum('ProfileVisibility', ['PRIVATE', 'ROOMMATES', 'RESIDENTS']) +export const proposalStatus = pgEnum('ProposalStatus', [ + 'DISCUSSION', + 'VOTING', + 'NEEDS_STAFF_CONFIRMATION', + 'ACCEPTED', + 'REJECTED', + 'WITHDRAWN', + 'VETOED', + 'EXPIRED', +]) +export const proposalType = pgEnum('ProposalType', [ + 'ADD_RULE', + 'AMEND_RULE', + 'REPEAL_RULE', + 'HOUSE_DECISION', +]) +export const recyclingKnowledge = pgEnum('RecyclingKnowledge', ['NONE', 'BASIC', 'GOOD']) +export const residentOrStaff = pgEnum('ResidentOrStaff', ['RESIDENT', 'STAFF']) +export const residentStatus = pgEnum('ResidentStatus', [ + 'ACTIVE', + 'PLACED', + 'TRANSFERRED', + 'EXITED', +]) +export const resolutionStage = pgEnum('ResolutionStage', [ + 'REPORTED', + 'SELF_RESOLUTION', + 'PEER_MEDIATION', + 'STAFF_MEDIATION', + 'FORMAL_MEASURE', + 'CLOSED', +]) +export const roomSharingStatus = pgEnum('RoomSharingStatus', [ + 'CAN_SHARE', + 'PREFERS_PRIVATE', + 'NEEDS_PRIVATE', +]) +export const ruleCategory = pgEnum('RuleCategory', [ + 'SAFETY', + 'RESPECT', + 'NOISE', + 'CLEANLINESS', + 'KITCHEN', + 'BATHROOM', + 'GUESTS', + 'SHARED_SPACES', + 'COSTS', + 'COMMUNICATION', + 'OTHER', +]) +export const ruleDelegation = pgEnum('RuleDelegation', [ + 'FIXED', + 'UNIT_MAY_STRENGTHEN', + 'UNIT_DECIDES', +]) +export const ruleScope = pgEnum('RuleScope', ['ORG', 'UNIT']) +export const ruleStatus = pgEnum('RuleStatus', ['ACTIVE', 'SUPERSEDED', 'ARCHIVED']) +export const sleepSchedule = pgEnum('SleepSchedule', [ + 'EARLY_BIRD', + 'STANDARD', + 'NIGHT_OWL', + 'IRREGULAR', +]) +export const smokingStatus = pgEnum('SmokingStatus', [ + 'NON_SMOKER', + 'OUTDOOR_SMOKER', + 'INDOOR_SMOKER', +]) +export const socialStyle = pgEnum('SocialStyle', ['INTROVERTED', 'MODERATE', 'EXTROVERTED']) +export const spotStatus = pgEnum('SpotStatus', ['AVAILABLE', 'OCCUPIED', 'MAINTENANCE', 'CLOSED']) +export const spotType = pgEnum('SpotType', ['BED', 'PRIVATE_ROOM', 'STUDIO', 'ROOM']) +export const staffDecision = pgEnum('StaffDecision', ['CONFIRMED', 'VETOED']) +export const staffRole = pgEnum('StaffRole', [ + 'ADMIN', + 'BETREUUNG', + 'SOZIALARBEIT', + 'JOBCOACH', + 'FREIWILLIGENARBEIT', +]) +export const staffScope = pgEnum('StaffScope', ['OWN_DOMAIN', 'ALL_DOMAINS']) +export const supportLevel = pgEnum('SupportLevel', ['STANDARD', 'ELEVATED', 'INTENSIVE']) +export const taskRequestStatus = pgEnum('TaskRequestStatus', [ + 'PENDING', + 'ACCEPTED', + 'DECLINED', + 'COMPLETED', +]) +export const transferRequestStatus = pgEnum('TransferRequestStatus', [ + 'PENDING', + 'APPROVED', + 'DENIED', + 'COMPLETED', + 'CANCELLED', +]) +export const voteChoice = pgEnum('VoteChoice', ['YES', 'NO', 'ABSTAIN', 'BLOCK']) +export const voteThreshold = pgEnum('VoteThreshold', [ + 'CONSENSUS', + 'SUPERMAJORITY', + 'SIMPLE_MAJORITY', +]) +export const algorithmWeight = pgTable( + 'AlgorithmWeight', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + lifestyleWeight: doublePrecision().default(30).notNull(), + socialWeight: doublePrecision().default(25).notNull(), + practicalWeight: doublePrecision().default(25).notNull(), + riskWeight: doublePrecision().default(20).notNull(), + factorWeights: jsonb().notNull(), + version: integer().default(1).notNull(), + active: boolean().default(true).notNull(), + notes: text(), + }, + (table) => [index('AlgorithmWeight_active_idx').using('btree', table.active.asc().nullsLast())], +) -export const algorithmWeight = pgTable("AlgorithmWeight", { - id: text().primaryKey().$defaultFn(createId).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - lifestyleWeight: doublePrecision().default(30).notNull(), - socialWeight: doublePrecision().default(25).notNull(), - practicalWeight: doublePrecision().default(25).notNull(), - riskWeight: doublePrecision().default(20).notNull(), - factorWeights: jsonb().notNull(), - version: integer().default(1).notNull(), - active: boolean().default(true).notNull(), - notes: text(), -}, (table) => [ - index("AlgorithmWeight_active_idx").using("btree", table.active.asc().nullsLast()), -]); +export const placementSpot = pgTable( + 'PlacementSpot', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + code: text().notNull(), + label: text(), + type: spotType().notNull(), + parentSpotId: text(), + squareMeters: doublePrecision(), + floor: integer(), + hasPrivateBathroom: boolean().default(false).notNull(), + hasPrivateKitchen: boolean().default(false).notNull(), + hasPrivateToilet: boolean().default(false).notNull(), + capacity: integer().default(1).notNull(), + requiresMedicalDocs: boolean().default(false).notNull(), + status: spotStatus().default('AVAILABLE').notNull(), + notes: text(), + }, + (table) => [ + uniqueIndex('PlacementSpot_housingUnitId_code_key').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.code.asc().nullsLast(), + ), + index('PlacementSpot_housingUnitId_idx').using('btree', table.housingUnitId.asc().nullsLast()), + index('PlacementSpot_requiresMedicalDocs_idx').using( + 'btree', + table.requiresMedicalDocs.asc().nullsLast(), + ), + index('PlacementSpot_type_status_idx').using( + 'btree', + table.type.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'PlacementSpot_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.parentSpotId], + foreignColumns: [table.id], + name: 'PlacementSpot_parentSpotId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const placementSpot = pgTable("PlacementSpot", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - code: text().notNull(), - label: text(), - type: spotType().notNull(), - parentSpotId: text(), - squareMeters: doublePrecision(), - floor: integer(), - hasPrivateBathroom: boolean().default(false).notNull(), - hasPrivateKitchen: boolean().default(false).notNull(), - hasPrivateToilet: boolean().default(false).notNull(), - capacity: integer().default(1).notNull(), - requiresMedicalDocs: boolean().default(false).notNull(), - status: spotStatus().default('AVAILABLE').notNull(), - notes: text(), -}, (table) => [ - uniqueIndex("PlacementSpot_housingUnitId_code_key").using("btree", table.housingUnitId.asc().nullsLast(), table.code.asc().nullsLast()), - index("PlacementSpot_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), - index("PlacementSpot_requiresMedicalDocs_idx").using("btree", table.requiresMedicalDocs.asc().nullsLast()), - index("PlacementSpot_type_status_idx").using("btree", table.type.asc().nullsLast(), table.status.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "PlacementSpot_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.parentSpotId], - foreignColumns: [table.id], - name: "PlacementSpot_parentSpotId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const placement = pgTable( + 'Placement', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + housingUnitId: text().notNull(), + spotId: text(), + startDate: timestamp({ precision: 3, mode: 'date' }).notNull(), + endDate: timestamp({ precision: 3, mode: 'date' }), + compatibilityScore: doublePrecision(), + lifestyleScore: doublePrecision(), + socialScore: doublePrecision(), + practicalScore: doublePrecision(), + riskScore: doublePrecision(), + status: placementStatus().default('ACTIVE').notNull(), + endReason: endReason(), + satisfactionRating: integer(), + placementNotes: text(), + outcomeNotes: text(), + conflictGap: text(), + wasPredictable: boolean(), + relatedIncidentId: text(), + }, + (table): PgTableExtraConfigValue[] => [ + index('Placement_housingUnitId_idx').using('btree', table.housingUnitId.asc().nullsLast()), + uniqueIndex('Placement_residentId_housingUnitId_startDate_key').using( + 'btree', + table.residentId.asc().nullsLast(), + table.housingUnitId.asc().nullsLast(), + table.startDate.asc().nullsLast(), + ), + index('Placement_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + index('Placement_startDate_endDate_idx').using( + 'btree', + table.startDate.asc().nullsLast(), + table.endDate.asc().nullsLast(), + ), + index('Placement_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'Placement_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'Placement_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.spotId], + foreignColumns: [placementSpot.id], + name: 'Placement_spotId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.relatedIncidentId], + // Cast breaks the type-level cycle (Placement <-> Incident reference + // each other); the generated DDL is unchanged. + foreignColumns: [incident.id as AnyPgColumn], + name: 'Placement_relatedIncidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const placement = pgTable("Placement", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - housingUnitId: text().notNull(), - spotId: text(), - startDate: timestamp({ precision: 3, mode: 'date' }).notNull(), - endDate: timestamp({ precision: 3, mode: 'date' }), - compatibilityScore: doublePrecision(), - lifestyleScore: doublePrecision(), - socialScore: doublePrecision(), - practicalScore: doublePrecision(), - riskScore: doublePrecision(), - status: placementStatus().default('ACTIVE').notNull(), - endReason: endReason(), - satisfactionRating: integer(), - placementNotes: text(), - outcomeNotes: text(), - conflictGap: text(), - wasPredictable: boolean(), - relatedIncidentId: text(), -}, (table) => [ - index("Placement_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), - uniqueIndex("Placement_residentId_housingUnitId_startDate_key").using("btree", table.residentId.asc().nullsLast(), table.housingUnitId.asc().nullsLast(), table.startDate.asc().nullsLast()), - index("Placement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - index("Placement_startDate_endDate_idx").using("btree", table.startDate.asc().nullsLast(), table.endDate.asc().nullsLast()), - index("Placement_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "Placement_residentId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "Placement_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.spotId], - foreignColumns: [placementSpot.id], - name: "Placement_spotId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.relatedIncidentId], - foreignColumns: [incident.id], - name: "Placement_relatedIncidentId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const compatibilityAssessment = pgTable( + 'CompatibilityAssessment', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + residentId: text().notNull(), + comparedWithId: text().notNull(), + overallScore: doublePrecision().notNull(), + lifestyleScore: doublePrecision().notNull(), + socialScore: doublePrecision().notNull(), + practicalScore: doublePrecision().notNull(), + riskScore: doublePrecision().notNull(), + strengths: text().array(), + concerns: text().array(), + recommendations: text().array(), + }, + (table) => [ + index('CompatibilityAssessment_overallScore_idx').using( + 'btree', + table.overallScore.asc().nullsLast(), + ), + uniqueIndex('CompatibilityAssessment_residentId_comparedWithId_key').using( + 'btree', + table.residentId.asc().nullsLast(), + table.comparedWithId.asc().nullsLast(), + ), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'CompatibilityAssessment_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.comparedWithId], + foreignColumns: [resident.id], + name: 'CompatibilityAssessment_comparedWithId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const compatibilityAssessment = pgTable("CompatibilityAssessment", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - residentId: text().notNull(), - comparedWithId: text().notNull(), - overallScore: doublePrecision().notNull(), - lifestyleScore: doublePrecision().notNull(), - socialScore: doublePrecision().notNull(), - practicalScore: doublePrecision().notNull(), - riskScore: doublePrecision().notNull(), - strengths: text().array(), - concerns: text().array(), - recommendations: text().array(), -}, (table) => [ - index("CompatibilityAssessment_overallScore_idx").using("btree", table.overallScore.asc().nullsLast()), - uniqueIndex("CompatibilityAssessment_residentId_comparedWithId_key").using("btree", table.residentId.asc().nullsLast(), table.comparedWithId.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "CompatibilityAssessment_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.comparedWithId], - foreignColumns: [resident.id], - name: "CompatibilityAssessment_comparedWithId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const incidentFollowUp = pgTable( + 'IncidentFollowUp', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + incidentId: text().notNull(), + action: text().notNull(), + notes: text(), + outcome: text(), + staffName: text(), + scheduledNextDate: timestamp({ precision: 3, mode: 'date' }), + }, + (table) => [ + index('IncidentFollowUp_createdAt_idx').using('btree', table.createdAt.asc().nullsLast()), + index('IncidentFollowUp_incidentId_idx').using('btree', table.incidentId.asc().nullsLast()), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: 'IncidentFollowUp_incidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const incidentFollowUp = pgTable("IncidentFollowUp", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - incidentId: text().notNull(), - action: text().notNull(), - notes: text(), - outcome: text(), - staffName: text(), - scheduledNextDate: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - index("IncidentFollowUp_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), - index("IncidentFollowUp_incidentId_idx").using("btree", table.incidentId.asc().nullsLast()), - foreignKey({ - columns: [table.incidentId], - foreignColumns: [incident.id], - name: "IncidentFollowUp_incidentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const incidentInvolvement = pgTable( + 'IncidentInvolvement', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + incidentId: text().notNull(), + residentId: text().notNull(), + role: involvementRole().default('INVOLVED').notNull(), + }, + (table) => [ + uniqueIndex('IncidentInvolvement_incidentId_residentId_key').using( + 'btree', + table.incidentId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + ), + index('IncidentInvolvement_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: 'IncidentInvolvement_incidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'IncidentInvolvement_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const incidentInvolvement = pgTable("IncidentInvolvement", { - id: text().primaryKey().$defaultFn(createId).notNull(), - incidentId: text().notNull(), - residentId: text().notNull(), - role: involvementRole().default('INVOLVED').notNull(), -}, (table) => [ - uniqueIndex("IncidentInvolvement_incidentId_residentId_key").using("btree", table.incidentId.asc().nullsLast(), table.residentId.asc().nullsLast()), - index("IncidentInvolvement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - foreignKey({ - columns: [table.incidentId], - foreignColumns: [incident.id], - name: "IncidentInvolvement_incidentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "IncidentInvolvement_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const auditLog = pgTable( + 'AuditLog', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + action: text().notNull(), + entity: text().notNull(), + entityId: text().notNull(), + userId: text(), + changes: jsonb(), + reason: text(), + }, + (table) => [ + index('AuditLog_createdAt_idx').using('btree', table.createdAt.asc().nullsLast()), + index('AuditLog_entity_entityId_idx').using( + 'btree', + table.entity.asc().nullsLast(), + table.entityId.asc().nullsLast(), + ), + index('AuditLog_userId_idx').using('btree', table.userId.asc().nullsLast()), + foreignKey({ + columns: [table.userId], + foreignColumns: [user.id], + name: 'AuditLog_userId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const auditLog = pgTable("AuditLog", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - action: text().notNull(), - entity: text().notNull(), - entityId: text().notNull(), - userId: text(), - changes: jsonb(), - reason: text(), -}, (table) => [ - index("AuditLog_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), - index("AuditLog_entity_entityId_idx").using("btree", table.entity.asc().nullsLast(), table.entityId.asc().nullsLast()), - index("AuditLog_userId_idx").using("btree", table.userId.asc().nullsLast()), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: "AuditLog_userId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const maintenanceRequest = pgTable( + 'MaintenanceRequest', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + spotId: text(), + category: maintenanceCategory().notNull(), + priority: maintenancePriority().default('NORMAL').notNull(), + title: text().notNull(), + description: text().notNull(), + location: text(), + reportedById: text(), + reporterName: text(), + assignedTo: text(), + assignedAt: timestamp({ precision: 3, mode: 'date' }), + status: maintenanceStatus().default('OPEN').notNull(), + startedAt: timestamp({ precision: 3, mode: 'date' }), + completedAt: timestamp({ precision: 3, mode: 'date' }), + resolution: text(), + cost: doublePrecision(), + notes: text(), + }, + (table) => [ + index('MaintenanceRequest_createdAt_idx').using('btree', table.createdAt.asc().nullsLast()), + index('MaintenanceRequest_housingUnitId_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + ), + index('MaintenanceRequest_priority_status_idx').using( + 'btree', + table.priority.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + index('MaintenanceRequest_reportedById_idx').using( + 'btree', + table.reportedById.asc().nullsLast(), + ), + index('MaintenanceRequest_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'MaintenanceRequest_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.spotId], + foreignColumns: [placementSpot.id], + name: 'MaintenanceRequest_spotId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.reportedById], + foreignColumns: [resident.id], + name: 'MaintenanceRequest_reportedById_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const maintenanceRequest = pgTable("MaintenanceRequest", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - spotId: text(), - category: maintenanceCategory().notNull(), - priority: maintenancePriority().default('NORMAL').notNull(), - title: text().notNull(), - description: text().notNull(), - location: text(), - reportedById: text(), - reporterName: text(), - assignedTo: text(), - assignedAt: timestamp({ precision: 3, mode: 'date' }), - status: maintenanceStatus().default('OPEN').notNull(), - startedAt: timestamp({ precision: 3, mode: 'date' }), - completedAt: timestamp({ precision: 3, mode: 'date' }), - resolution: text(), - cost: doublePrecision(), - notes: text(), -}, (table) => [ - index("MaintenanceRequest_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), - index("MaintenanceRequest_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), - index("MaintenanceRequest_priority_status_idx").using("btree", table.priority.asc().nullsLast(), table.status.asc().nullsLast()), - index("MaintenanceRequest_reportedById_idx").using("btree", table.reportedById.asc().nullsLast()), - index("MaintenanceRequest_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "MaintenanceRequest_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.spotId], - foreignColumns: [placementSpot.id], - name: "MaintenanceRequest_spotId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.reportedById], - foreignColumns: [resident.id], - name: "MaintenanceRequest_reportedById_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const taskAttentionFlag = pgTable( + 'TaskAttentionFlag', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + taskId: text().notNull(), + flaggedById: text().notNull(), + message: text(), + isResolved: boolean().default(false).notNull(), + resolvedAt: timestamp({ precision: 3, mode: 'date' }), + resolvedByCompletionId: text(), + }, + (table) => [ + index('TaskAttentionFlag_taskId_idx').using('btree', table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: 'TaskAttentionFlag_taskId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.flaggedById], + foreignColumns: [resident.id], + name: 'TaskAttentionFlag_flaggedById_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.resolvedByCompletionId], + foreignColumns: [taskCompletion.id], + name: 'TaskAttentionFlag_resolvedByCompletionId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const taskAttentionFlag = pgTable("TaskAttentionFlag", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - taskId: text().notNull(), - flaggedById: text().notNull(), - message: text(), - isResolved: boolean().default(false).notNull(), - resolvedAt: timestamp({ precision: 3, mode: 'date' }), - resolvedByCompletionId: text(), -}, (table) => [ - index("TaskAttentionFlag_taskId_idx").using("btree", table.taskId.asc().nullsLast()), - foreignKey({ - columns: [table.taskId], - foreignColumns: [householdTask.id], - name: "TaskAttentionFlag_taskId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.flaggedById], - foreignColumns: [resident.id], - name: "TaskAttentionFlag_flaggedById_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.resolvedByCompletionId], - foreignColumns: [taskCompletion.id], - name: "TaskAttentionFlag_resolvedByCompletionId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const housingUnit = pgTable( + 'HousingUnit', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + code: text().notNull(), + address: text().notNull(), + totalBeds: integer().notNull(), + totalRooms: integer().notNull(), + sharedRooms: integer().notNull(), + privateRooms: integer().notNull(), + sharedBathrooms: integer().notNull(), + privateBathrooms: integer().notNull(), + sharedKitchen: boolean().default(true).notNull(), + privateKitchen: boolean().default(false).notNull(), + groundFloor: boolean().default(false).notNull(), + wheelchairAccess: boolean().default(false).notNull(), + elevator: boolean().default(false).notNull(), + smokingAllowed: boolean().default(false).notNull(), + petsAllowed: boolean().default(false).notNull(), + quietHours: text(), + nearPublicTransport: boolean().default(true).notNull(), + nearHealthServices: boolean().default(false).notNull(), + nearSchools: boolean().default(false).notNull(), + status: housingStatus().default('AVAILABLE').notNull(), + notes: text(), + nickname: text(), + buildingCode: text(), + }, + (table) => [ + index('HousingUnit_buildingCode_idx').using('btree', table.buildingCode.asc().nullsLast()), + uniqueIndex('HousingUnit_code_key').using('btree', table.code.asc().nullsLast()), + index('HousingUnit_status_idx').using('btree', table.status.asc().nullsLast()), + index('HousingUnit_totalBeds_idx').using('btree', table.totalBeds.asc().nullsLast()), + ], +) -export const housingUnit = pgTable("HousingUnit", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - code: text().notNull(), - address: text().notNull(), - totalBeds: integer().notNull(), - totalRooms: integer().notNull(), - sharedRooms: integer().notNull(), - privateRooms: integer().notNull(), - sharedBathrooms: integer().notNull(), - privateBathrooms: integer().notNull(), - sharedKitchen: boolean().default(true).notNull(), - privateKitchen: boolean().default(false).notNull(), - groundFloor: boolean().default(false).notNull(), - wheelchairAccess: boolean().default(false).notNull(), - elevator: boolean().default(false).notNull(), - smokingAllowed: boolean().default(false).notNull(), - petsAllowed: boolean().default(false).notNull(), - quietHours: text(), - nearPublicTransport: boolean().default(true).notNull(), - nearHealthServices: boolean().default(false).notNull(), - nearSchools: boolean().default(false).notNull(), - status: housingStatus().default('AVAILABLE').notNull(), - notes: text(), - nickname: text(), - buildingCode: text(), -}, (table) => [ - index("HousingUnit_buildingCode_idx").using("btree", table.buildingCode.asc().nullsLast()), - uniqueIndex("HousingUnit_code_key").using("btree", table.code.asc().nullsLast()), - index("HousingUnit_status_idx").using("btree", table.status.asc().nullsLast()), - index("HousingUnit_totalBeds_idx").using("btree", table.totalBeds.asc().nullsLast()), -]); +export const householdTask = pgTable( + 'HouseholdTask', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + title: text().notNull(), + description: text(), + instructions: text(), + taskType: householdTaskType().default('ONE_TIME').notNull(), + category: householdTaskCategory().default('OTHER').notNull(), + priority: householdTaskPriority().default('NORMAL').notNull(), + scheduleHuman: text(), + estimatedMinutes: integer(), + currentStatus: householdTaskStatus().default('IDLE').notNull(), + isCompleted: boolean().default(false).notNull(), + completedAt: timestamp({ precision: 3, mode: 'date' }), + createdByResidentId: text(), + createdByStaff: text(), + checklist: text() + .array() + .default(sql`ARRAY[]::TEXT[]`), + rotationResidentIds: text() + .array() + .default(sql`ARRAY[]::TEXT[]`), + }, + (table) => [ + index('HouseholdTask_housingUnitId_category_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.category.asc().nullsLast(), + ), + index('HouseholdTask_housingUnitId_currentStatus_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.currentStatus.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'HouseholdTask_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.createdByResidentId], + foreignColumns: [resident.id], + name: 'HouseholdTask_createdByResidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const householdTask = pgTable("HouseholdTask", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - title: text().notNull(), - description: text(), - instructions: text(), - taskType: householdTaskType().default('ONE_TIME').notNull(), - category: householdTaskCategory().default('OTHER').notNull(), - priority: householdTaskPriority().default('NORMAL').notNull(), - scheduleHuman: text(), - estimatedMinutes: integer(), - currentStatus: householdTaskStatus().default('IDLE').notNull(), - isCompleted: boolean().default(false).notNull(), - completedAt: timestamp({ precision: 3, mode: 'date' }), - createdByResidentId: text(), - createdByStaff: text(), - checklist: text().array().default(sql`ARRAY[]::TEXT[]`), - rotationResidentIds: text().array().default(sql`ARRAY[]::TEXT[]`), -}, (table) => [ - index("HouseholdTask_housingUnitId_category_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.category.asc().nullsLast()), - index("HouseholdTask_housingUnitId_currentStatus_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.currentStatus.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "HouseholdTask_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.createdByResidentId], - foreignColumns: [resident.id], - name: "HouseholdTask_createdByResidentId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const satisfactionCheckIn = pgTable( + 'SatisfactionCheckIn', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + placementId: text().notNull(), + checkInType: checkInType().notNull(), + weekNumber: integer(), + overallSatisfaction: integer().notNull(), + roommateRelations: integer(), + facilitySatisfaction: integer(), + safetyFeeling: integer(), + concerns: text(), + improvements: text(), + positives: text(), + collectedBy: text(), + isAnonymous: boolean().default(false).notNull(), + appointmentId: text(), + collectedByUserId: text(), + }, + (table) => [ + uniqueIndex('SatisfactionCheckIn_appointmentId_key').using( + 'btree', + table.appointmentId.asc().nullsLast(), + ), + index('SatisfactionCheckIn_checkInType_idx').using( + 'btree', + table.checkInType.asc().nullsLast(), + ), + index('SatisfactionCheckIn_collectedByUserId_idx').using( + 'btree', + table.collectedByUserId.asc().nullsLast(), + ), + index('SatisfactionCheckIn_placementId_idx').using( + 'btree', + table.placementId.asc().nullsLast(), + ), + foreignKey({ + columns: [table.placementId], + foreignColumns: [placement.id], + name: 'SatisfactionCheckIn_placementId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.appointmentId], + foreignColumns: [appointment.id], + name: 'SatisfactionCheckIn_appointmentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.collectedByUserId], + foreignColumns: [user.id], + name: 'SatisfactionCheckIn_collectedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const satisfactionCheckIn = pgTable("SatisfactionCheckIn", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - placementId: text().notNull(), - checkInType: checkInType().notNull(), - weekNumber: integer(), - overallSatisfaction: integer().notNull(), - roommateRelations: integer(), - facilitySatisfaction: integer(), - safetyFeeling: integer(), - concerns: text(), - improvements: text(), - positives: text(), - collectedBy: text(), - isAnonymous: boolean().default(false).notNull(), - appointmentId: text(), - collectedByUserId: text(), -}, (table) => [ - uniqueIndex("SatisfactionCheckIn_appointmentId_key").using("btree", table.appointmentId.asc().nullsLast()), - index("SatisfactionCheckIn_checkInType_idx").using("btree", table.checkInType.asc().nullsLast()), - index("SatisfactionCheckIn_collectedByUserId_idx").using("btree", table.collectedByUserId.asc().nullsLast()), - index("SatisfactionCheckIn_placementId_idx").using("btree", table.placementId.asc().nullsLast()), - foreignKey({ - columns: [table.placementId], - foreignColumns: [placement.id], - name: "SatisfactionCheckIn_placementId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.appointmentId], - foreignColumns: [appointment.id], - name: "SatisfactionCheckIn_appointmentId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.collectedByUserId], - foreignColumns: [user.id], - name: "SatisfactionCheckIn_collectedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const user = pgTable( + 'User', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + name: text().notNull(), + role: staffRole().default('BETREUUNG').notNull(), + active: boolean().default(true).notNull(), + lastLoginAt: timestamp({ precision: 3, mode: 'date' }), + code: text().notNull(), + scope: staffScope().default('OWN_DOMAIN').notNull(), + isSystemAdmin: boolean().default(false).notNull(), + }, + (table) => [ + index('User_code_idx').using('btree', table.code.asc().nullsLast()), + index('User_role_idx').using('btree', table.role.asc().nullsLast()), + index('User_scope_idx').using('btree', table.scope.asc().nullsLast()), + unique('User_code_key').on(table.code), + ], +) -export const user = pgTable("User", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - name: text().notNull(), - role: staffRole().default('BETREUUNG').notNull(), - active: boolean().default(true).notNull(), - lastLoginAt: timestamp({ precision: 3, mode: 'date' }), - code: text().notNull(), - scope: staffScope().default('OWN_DOMAIN').notNull(), - isSystemAdmin: boolean().default(false).notNull(), -}, (table) => [ - index("User_code_idx").using("btree", table.code.asc().nullsLast()), - index("User_role_idx").using("btree", table.role.asc().nullsLast()), - index("User_scope_idx").using("btree", table.scope.asc().nullsLast()), - unique("User_code_key").on(table.code), -]); +export const taskRequest = pgTable( + 'TaskRequest', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + taskId: text().notNull(), + requestedById: text().notNull(), + requestedResidentId: text(), + isBroadcast: boolean().default(false).notNull(), + message: text(), + status: taskRequestStatus().default('PENDING').notNull(), + responseMessage: text(), + completionId: text(), + }, + (table) => [ + index('TaskRequest_requestedResidentId_idx').using( + 'btree', + table.requestedResidentId.asc().nullsLast(), + ), + index('TaskRequest_taskId_idx').using('btree', table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: 'TaskRequest_taskId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.requestedById], + foreignColumns: [resident.id], + name: 'TaskRequest_requestedById_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.requestedResidentId], + foreignColumns: [resident.id], + name: 'TaskRequest_requestedResidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.completionId], + foreignColumns: [taskCompletion.id], + name: 'TaskRequest_completionId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const taskRequest = pgTable("TaskRequest", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - taskId: text().notNull(), - requestedById: text().notNull(), - requestedResidentId: text(), - isBroadcast: boolean().default(false).notNull(), - message: text(), - status: taskRequestStatus().default('PENDING').notNull(), - responseMessage: text(), - completionId: text(), -}, (table) => [ - index("TaskRequest_requestedResidentId_idx").using("btree", table.requestedResidentId.asc().nullsLast()), - index("TaskRequest_taskId_idx").using("btree", table.taskId.asc().nullsLast()), - foreignKey({ - columns: [table.taskId], - foreignColumns: [householdTask.id], - name: "TaskRequest_taskId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.requestedById], - foreignColumns: [resident.id], - name: "TaskRequest_requestedById_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.requestedResidentId], - foreignColumns: [resident.id], - name: "TaskRequest_requestedResidentId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.completionId], - foreignColumns: [taskCompletion.id], - name: "TaskRequest_completionId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const transferRequest = pgTable( + 'TransferRequest', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + currentPlacementId: text(), + targetUnitId: text(), + reason: text().notNull(), + status: transferRequestStatus().default('PENDING').notNull(), + staffNotes: text(), + reviewedBy: text(), + reviewedAt: timestamp({ precision: 3, mode: 'date' }), + }, + (table) => [ + index('TransferRequest_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + index('TransferRequest_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'TransferRequest_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.currentPlacementId], + foreignColumns: [placement.id], + name: 'TransferRequest_currentPlacementId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.targetUnitId], + foreignColumns: [housingUnit.id], + name: 'TransferRequest_targetUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const transferRequest = pgTable("TransferRequest", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - currentPlacementId: text(), - targetUnitId: text(), - reason: text().notNull(), - status: transferRequestStatus().default('PENDING').notNull(), - staffNotes: text(), - reviewedBy: text(), - reviewedAt: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - index("TransferRequest_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - index("TransferRequest_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "TransferRequest_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.currentPlacementId], - foreignColumns: [placement.id], - name: "TransferRequest_currentPlacementId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.targetUnitId], - foreignColumns: [housingUnit.id], - name: "TransferRequest_targetUnitId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const resident = pgTable( + 'Resident', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + code: text().notNull(), + ageRange: ageRange().notNull(), + gender: gender().notNull(), + familyStatus: familyStatus().notNull(), + sleepSchedule: sleepSchedule().notNull(), + noiseTolerance: integer().notNull(), + cleanlinessPractice: integer().notNull(), + guestTolerance: integer().default(3).notNull(), + socialStyle: socialStyle().notNull(), + languages: text().array(), + culturalRegion: text(), + conflictStyle: conflictStyle().default('COOPERATIVE').notNull(), + smokingStatus: smokingStatus().notNull(), + dietaryNeeds: text().array(), + mobilityNeeds: mobilityNeed().notNull(), + medicalEquipment: boolean().default(false).notNull(), + petTolerance: boolean().default(true).notNull(), + sharedBathroom: boolean().default(true).notNull(), + sharedKitchen: boolean().default(true).notNull(), + privacyNeed: integer().notNull(), + choresContribution: integer().default(3).notNull(), + recyclingKnowledge: recyclingKnowledge().default('NONE').notNull(), + roomSharingStatus: roomSharingStatus().default('CAN_SHARE').notNull(), + hasNightDisturbances: boolean().default(false).notNull(), + needsQuietEnvironment: boolean().default(false).notNull(), + hasSleepEquipment: boolean().default(false).notNull(), + supportLevel: supportLevel().default('STANDARD').notNull(), + roommatePreferences: text(), + status: residentStatus().default('ACTIVE').notNull(), + notes: text(), + hasMedicalDocumentation: boolean().default(false).notNull(), + medicalDocType: medicalDocType(), + medicalDocDate: timestamp({ precision: 3, mode: 'date' }), + medicalDocNotes: text(), + preferencesCompletedAt: timestamp({ precision: 3, mode: 'date' }), + cleanlinessExpectation: integer().default(3).notNull(), + chaosTolerance: integer().default(3).notNull(), + bio: text(), + displayName: text(), + profileVisibility: profileVisibility().default('ROOMMATES').notNull(), + livingSkillsSupport: livingSkillsSupport().default('INDEPENDENT').notNull(), + }, + (table) => [ + index('Resident_ageRange_gender_idx').using( + 'btree', + table.ageRange.asc().nullsLast(), + table.gender.asc().nullsLast(), + ), + uniqueIndex('Resident_code_key').using('btree', table.code.asc().nullsLast()), + index('Resident_livingSkillsSupport_idx').using( + 'btree', + table.livingSkillsSupport.asc().nullsLast(), + ), + index('Resident_status_idx').using('btree', table.status.asc().nullsLast()), + ], +) -export const resident = pgTable("Resident", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - code: text().notNull(), - ageRange: ageRange().notNull(), - gender: gender().notNull(), - familyStatus: familyStatus().notNull(), - sleepSchedule: sleepSchedule().notNull(), - noiseTolerance: integer().notNull(), - cleanlinessPractice: integer().notNull(), - guestTolerance: integer().default(3).notNull(), - socialStyle: socialStyle().notNull(), - languages: text().array(), - culturalRegion: text(), - conflictStyle: conflictStyle().default('COOPERATIVE').notNull(), - smokingStatus: smokingStatus().notNull(), - dietaryNeeds: text().array(), - mobilityNeeds: mobilityNeed().notNull(), - medicalEquipment: boolean().default(false).notNull(), - petTolerance: boolean().default(true).notNull(), - sharedBathroom: boolean().default(true).notNull(), - sharedKitchen: boolean().default(true).notNull(), - privacyNeed: integer().notNull(), - choresContribution: integer().default(3).notNull(), - recyclingKnowledge: recyclingKnowledge().default('NONE').notNull(), - roomSharingStatus: roomSharingStatus().default('CAN_SHARE').notNull(), - hasNightDisturbances: boolean().default(false).notNull(), - needsQuietEnvironment: boolean().default(false).notNull(), - hasSleepEquipment: boolean().default(false).notNull(), - supportLevel: supportLevel().default('STANDARD').notNull(), - roommatePreferences: text(), - status: residentStatus().default('ACTIVE').notNull(), - notes: text(), - hasMedicalDocumentation: boolean().default(false).notNull(), - medicalDocType: medicalDocType(), - medicalDocDate: timestamp({ precision: 3, mode: 'date' }), - medicalDocNotes: text(), - preferencesCompletedAt: timestamp({ precision: 3, mode: 'date' }), - cleanlinessExpectation: integer().default(3).notNull(), - chaosTolerance: integer().default(3).notNull(), - bio: text(), - displayName: text(), - profileVisibility: profileVisibility().default('ROOMMATES').notNull(), - livingSkillsSupport: livingSkillsSupport().default('INDEPENDENT').notNull(), -}, (table) => [ - index("Resident_ageRange_gender_idx").using("btree", table.ageRange.asc().nullsLast(), table.gender.asc().nullsLast()), - uniqueIndex("Resident_code_key").using("btree", table.code.asc().nullsLast()), - index("Resident_livingSkillsSupport_idx").using("btree", table.livingSkillsSupport.asc().nullsLast()), - index("Resident_status_idx").using("btree", table.status.asc().nullsLast()), -]); +export const activity = pgTable( + 'Activity', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + title: text().notNull(), + description: text().notNull(), + category: activityCategory().notNull(), + cost: activityCost().default('FREE').notNull(), + costNote: text(), + location: text(), + website: text(), + phone: text(), + schedule: text(), + startsAt: timestamp({ precision: 3, mode: 'date' }), + endsAt: timestamp({ precision: 3, mode: 'date' }), + status: activityStatus().default('DRAFT').notNull(), + highlight: boolean().default(false).notNull(), + createdByUserId: text(), + updatedByUserId: text(), + }, + (table) => [ + index('Activity_endsAt_idx').using('btree', table.endsAt.asc().nullsLast()), + index('Activity_status_category_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.category.asc().nullsLast(), + ), + index('Activity_status_highlight_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.highlight.asc().nullsLast(), + ), + foreignKey({ + columns: [table.createdByUserId], + foreignColumns: [user.id], + name: 'Activity_createdByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.updatedByUserId], + foreignColumns: [user.id], + name: 'Activity_updatedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const activity = pgTable("Activity", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - title: text().notNull(), - description: text().notNull(), - category: activityCategory().notNull(), - cost: activityCost().default('FREE').notNull(), - costNote: text(), - location: text(), - website: text(), - phone: text(), - schedule: text(), - startsAt: timestamp({ precision: 3, mode: 'date' }), - endsAt: timestamp({ precision: 3, mode: 'date' }), - status: activityStatus().default('DRAFT').notNull(), - highlight: boolean().default(false).notNull(), - createdByUserId: text(), - updatedByUserId: text(), -}, (table) => [ - index("Activity_endsAt_idx").using("btree", table.endsAt.asc().nullsLast()), - index("Activity_status_category_idx").using("btree", table.status.asc().nullsLast(), table.category.asc().nullsLast()), - index("Activity_status_highlight_idx").using("btree", table.status.asc().nullsLast(), table.highlight.asc().nullsLast()), - foreignKey({ - columns: [table.createdByUserId], - foreignColumns: [user.id], - name: "Activity_createdByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.updatedByUserId], - foreignColumns: [user.id], - name: "Activity_updatedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); - -export const systemConfig = pgTable("SystemConfig", { - id: text().default('singleton').primaryKey().notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - pilotBaselineIncidentsPerMonth: doublePrecision(), - pilotBaselineRelocationsPerMonth: doublePrecision(), - pilotBaselineMediationHoursPerWeek: doublePrecision(), - pilotStartDate: timestamp({ precision: 3, mode: 'date' }), -}); +export const systemConfig = pgTable('SystemConfig', { + id: text().default('singleton').primaryKey().notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + pilotBaselineIncidentsPerMonth: doublePrecision(), + pilotBaselineRelocationsPerMonth: doublePrecision(), + pilotBaselineMediationHoursPerWeek: doublePrecision(), + pilotStartDate: timestamp({ precision: 3, mode: 'date' }), +}) -export const incident = pgTable("Incident", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - placementId: text(), - reportedById: text(), - subjectId: text(), - date: timestamp({ precision: 3, mode: 'date' }).notNull(), - category: incidentCategory().default('INTERPERSONAL').notNull(), - type: incidentType().notNull(), - severity: incidentSeverity().notNull(), - description: text().notNull(), - resolution: text(), - resolvedAt: timestamp({ precision: 3, mode: 'date' }), - predictable: boolean(), - compatibilityGap: text(), - nextFollowUpDate: timestamp({ precision: 3, mode: 'date' }), - followUpPriority: followUpPriority(), - mediationMinutes: integer(), - resolutionStage: resolutionStage().default('REPORTED').notNull(), - stageEnteredAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), -}, (table) => [ - index("Incident_date_idx").using("btree", table.date.asc().nullsLast()), - index("Incident_nextFollowUpDate_idx").using("btree", table.nextFollowUpDate.asc().nullsLast()), - index("Incident_reportedById_idx").using("btree", table.reportedById.asc().nullsLast()), - index("Incident_subjectId_idx").using("btree", table.subjectId.asc().nullsLast()), - index("Incident_type_severity_idx").using("btree", table.type.asc().nullsLast(), table.severity.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "Incident_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.placementId], - foreignColumns: [placement.id], - name: "Incident_placementId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.reportedById], - foreignColumns: [resident.id], - name: "Incident_reportedById_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.subjectId], - foreignColumns: [resident.id], - name: "Incident_subjectId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const incident = pgTable( + 'Incident', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + placementId: text(), + reportedById: text(), + subjectId: text(), + date: timestamp({ precision: 3, mode: 'date' }).notNull(), + category: incidentCategory().default('INTERPERSONAL').notNull(), + type: incidentType().notNull(), + severity: incidentSeverity().notNull(), + description: text().notNull(), + resolution: text(), + resolvedAt: timestamp({ precision: 3, mode: 'date' }), + predictable: boolean(), + compatibilityGap: text(), + nextFollowUpDate: timestamp({ precision: 3, mode: 'date' }), + followUpPriority: followUpPriority(), + mediationMinutes: integer(), + resolutionStage: resolutionStage().default('REPORTED').notNull(), + stageEnteredAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + }, + (table): PgTableExtraConfigValue[] => [ + index('Incident_date_idx').using('btree', table.date.asc().nullsLast()), + index('Incident_nextFollowUpDate_idx').using('btree', table.nextFollowUpDate.asc().nullsLast()), + index('Incident_reportedById_idx').using('btree', table.reportedById.asc().nullsLast()), + index('Incident_subjectId_idx').using('btree', table.subjectId.asc().nullsLast()), + index('Incident_type_severity_idx').using( + 'btree', + table.type.asc().nullsLast(), + table.severity.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'Incident_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.placementId], + foreignColumns: [placement.id], + name: 'Incident_placementId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.reportedById], + foreignColumns: [resident.id], + name: 'Incident_reportedById_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.subjectId], + foreignColumns: [resident.id], + name: 'Incident_subjectId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const houseRule = pgTable("HouseRule", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - scope: ruleScope().notNull(), - housingUnitId: text(), - key: text(), - category: ruleCategory().notNull(), - title: text().notNull(), - body: text().notNull(), - delegation: ruleDelegation().default('FIXED').notNull(), - parentRuleId: text(), - status: ruleStatus().default('ACTIVE').notNull(), - version: integer().default(1).notNull(), - effectiveFrom: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - effectiveUntil: timestamp({ precision: 3, mode: 'date' }), - adoptedByProposalId: text(), - createdByStaff: text(), -}, (table) => [ - index("HouseRule_category_idx").using("btree", table.category.asc().nullsLast()), - index("HouseRule_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), - uniqueIndex("HouseRule_key_key").using("btree", table.key.asc().nullsLast()), - index("HouseRule_parentRuleId_idx").using("btree", table.parentRuleId.asc().nullsLast()), - index("HouseRule_scope_status_idx").using("btree", table.scope.asc().nullsLast(), table.status.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "HouseRule_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.parentRuleId], - foreignColumns: [table.id], - name: "HouseRule_parentRuleId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.adoptedByProposalId], - foreignColumns: [proposal.id], - name: "HouseRule_adoptedByProposalId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const houseRule = pgTable( + 'HouseRule', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + scope: ruleScope().notNull(), + housingUnitId: text(), + key: text(), + category: ruleCategory().notNull(), + title: text().notNull(), + body: text().notNull(), + delegation: ruleDelegation().default('FIXED').notNull(), + parentRuleId: text(), + status: ruleStatus().default('ACTIVE').notNull(), + version: integer().default(1).notNull(), + effectiveFrom: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + effectiveUntil: timestamp({ precision: 3, mode: 'date' }), + adoptedByProposalId: text(), + createdByStaff: text(), + }, + (table): PgTableExtraConfigValue[] => [ + index('HouseRule_category_idx').using('btree', table.category.asc().nullsLast()), + index('HouseRule_housingUnitId_status_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + uniqueIndex('HouseRule_key_key').using('btree', table.key.asc().nullsLast()), + index('HouseRule_parentRuleId_idx').using('btree', table.parentRuleId.asc().nullsLast()), + index('HouseRule_scope_status_idx').using( + 'btree', + table.scope.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'HouseRule_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.parentRuleId], + foreignColumns: [table.id], + name: 'HouseRule_parentRuleId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.adoptedByProposalId], + // Cast breaks the type-level cycle (HouseRule <-> Proposal reference + // each other); the generated DDL is unchanged. + foreignColumns: [proposal.id as AnyPgColumn], + name: 'HouseRule_adoptedByProposalId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const proposal = pgTable("Proposal", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - type: proposalType().notNull(), - category: ruleCategory().notNull(), - title: text().notNull(), - body: text().notNull(), - targetRuleId: text(), - parentOrgRuleId: text(), - proposedByResidentId: text(), - proposedByStaff: text(), - status: proposalStatus().default('DISCUSSION').notNull(), - decisionMode: decisionMode().notNull(), - threshold: voteThreshold().notNull(), - quorumPercent: integer().notNull(), - approvalPercent: integer().notNull(), - eligibleVoterCount: integer().default(0).notNull(), - discussionEndsAt: timestamp({ precision: 3, mode: 'date' }), - votingOpenedAt: timestamp({ precision: 3, mode: 'date' }), - votingEndsAt: timestamp({ precision: 3, mode: 'date' }), - decidedAt: timestamp({ precision: 3, mode: 'date' }), - outcomeSummary: text(), - staffDecision: staffDecision(), - staffNotes: text(), - staffUserId: text(), - staffDecidedAt: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - index("Proposal_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), - index("Proposal_status_votingEndsAt_idx").using("btree", table.status.asc().nullsLast(), table.votingEndsAt.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "Proposal_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.targetRuleId], - foreignColumns: [houseRule.id], - name: "Proposal_targetRuleId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.parentOrgRuleId], - foreignColumns: [houseRule.id], - name: "Proposal_parentOrgRuleId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.proposedByResidentId], - foreignColumns: [resident.id], - name: "Proposal_proposedByResidentId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const proposal = pgTable( + 'Proposal', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + type: proposalType().notNull(), + category: ruleCategory().notNull(), + title: text().notNull(), + body: text().notNull(), + targetRuleId: text(), + parentOrgRuleId: text(), + proposedByResidentId: text(), + proposedByStaff: text(), + status: proposalStatus().default('DISCUSSION').notNull(), + decisionMode: decisionMode().notNull(), + threshold: voteThreshold().notNull(), + quorumPercent: integer().notNull(), + approvalPercent: integer().notNull(), + eligibleVoterCount: integer().default(0).notNull(), + discussionEndsAt: timestamp({ precision: 3, mode: 'date' }), + votingOpenedAt: timestamp({ precision: 3, mode: 'date' }), + votingEndsAt: timestamp({ precision: 3, mode: 'date' }), + decidedAt: timestamp({ precision: 3, mode: 'date' }), + outcomeSummary: text(), + staffDecision: staffDecision(), + staffNotes: text(), + staffUserId: text(), + staffDecidedAt: timestamp({ precision: 3, mode: 'date' }), + }, + (table): PgTableExtraConfigValue[] => [ + index('Proposal_housingUnitId_status_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + index('Proposal_status_votingEndsAt_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.votingEndsAt.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'Proposal_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.targetRuleId], + foreignColumns: [houseRule.id], + name: 'Proposal_targetRuleId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.parentOrgRuleId], + foreignColumns: [houseRule.id], + name: 'Proposal_parentOrgRuleId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.proposedByResidentId], + foreignColumns: [resident.id], + name: 'Proposal_proposedByResidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const ruleAcknowledgement = pgTable("RuleAcknowledgement", { - id: text().primaryKey().$defaultFn(createId).notNull(), - ruleId: text().notNull(), - residentId: text().notNull(), - ruleVersion: integer().notNull(), - acknowledgedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), -}, (table) => [ - index("RuleAcknowledgement_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - uniqueIndex("RuleAcknowledgement_ruleId_residentId_ruleVersion_key").using("btree", table.ruleId.asc().nullsLast(), table.residentId.asc().nullsLast(), table.ruleVersion.asc().nullsLast()), - foreignKey({ - columns: [table.ruleId], - foreignColumns: [houseRule.id], - name: "RuleAcknowledgement_ruleId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "RuleAcknowledgement_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const ruleAcknowledgement = pgTable( + 'RuleAcknowledgement', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + ruleId: text().notNull(), + residentId: text().notNull(), + ruleVersion: integer().notNull(), + acknowledgedAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + }, + (table) => [ + index('RuleAcknowledgement_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + uniqueIndex('RuleAcknowledgement_ruleId_residentId_ruleVersion_key').using( + 'btree', + table.ruleId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + table.ruleVersion.asc().nullsLast(), + ), + foreignKey({ + columns: [table.ruleId], + foreignColumns: [houseRule.id], + name: 'RuleAcknowledgement_ruleId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'RuleAcknowledgement_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const vote = pgTable("Vote", { - id: text().primaryKey().$defaultFn(createId).notNull(), - proposalId: text().notNull(), - residentId: text().notNull(), - choice: voteChoice().notNull(), - reason: text(), - castAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), -}, (table) => [ - uniqueIndex("Vote_proposalId_residentId_key").using("btree", table.proposalId.asc().nullsLast(), table.residentId.asc().nullsLast()), - index("Vote_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - foreignKey({ - columns: [table.proposalId], - foreignColumns: [proposal.id], - name: "Vote_proposalId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "Vote_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const vote = pgTable( + 'Vote', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + proposalId: text().notNull(), + residentId: text().notNull(), + choice: voteChoice().notNull(), + reason: text(), + castAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + }, + (table) => [ + uniqueIndex('Vote_proposalId_residentId_key').using( + 'btree', + table.proposalId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + ), + index('Vote_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.proposalId], + foreignColumns: [proposal.id], + name: 'Vote_proposalId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'Vote_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const conflictAgreement = pgTable("ConflictAgreement", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - incidentId: text().notNull(), - terms: text().notNull(), - mediatorName: text(), - reviewDate: timestamp({ precision: 3, mode: 'date' }).notNull(), - status: agreementStatus().default('PROPOSED').notNull(), - outcomeNotes: text(), - reviewedAt: timestamp({ precision: 3, mode: 'date' }), - ruleProposalId: text(), -}, (table) => [ - index("ConflictAgreement_incidentId_idx").using("btree", table.incidentId.asc().nullsLast()), - uniqueIndex("ConflictAgreement_ruleProposalId_key").using("btree", table.ruleProposalId.asc().nullsLast()), - index("ConflictAgreement_status_reviewDate_idx").using("btree", table.status.asc().nullsLast(), table.reviewDate.asc().nullsLast()), - foreignKey({ - columns: [table.incidentId], - foreignColumns: [incident.id], - name: "ConflictAgreement_incidentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.ruleProposalId], - foreignColumns: [proposal.id], - name: "ConflictAgreement_ruleProposalId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const conflictAgreement = pgTable( + 'ConflictAgreement', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + incidentId: text().notNull(), + terms: text().notNull(), + mediatorName: text(), + reviewDate: timestamp({ precision: 3, mode: 'date' }).notNull(), + status: agreementStatus().default('PROPOSED').notNull(), + outcomeNotes: text(), + reviewedAt: timestamp({ precision: 3, mode: 'date' }), + ruleProposalId: text(), + }, + (table) => [ + index('ConflictAgreement_incidentId_idx').using('btree', table.incidentId.asc().nullsLast()), + uniqueIndex('ConflictAgreement_ruleProposalId_key').using( + 'btree', + table.ruleProposalId.asc().nullsLast(), + ), + index('ConflictAgreement_status_reviewDate_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.reviewDate.asc().nullsLast(), + ), + foreignKey({ + columns: [table.incidentId], + foreignColumns: [incident.id], + name: 'ConflictAgreement_incidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.ruleProposalId], + foreignColumns: [proposal.id], + name: 'ConflictAgreement_ruleProposalId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const agreementParty = pgTable("AgreementParty", { - id: text().primaryKey().$defaultFn(createId).notNull(), - agreementId: text().notNull(), - residentId: text().notNull(), - acceptedAt: timestamp({ precision: 3, mode: 'date' }), - declinedAt: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - uniqueIndex("AgreementParty_agreementId_residentId_key").using("btree", table.agreementId.asc().nullsLast(), table.residentId.asc().nullsLast()), - index("AgreementParty_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - foreignKey({ - columns: [table.agreementId], - foreignColumns: [conflictAgreement.id], - name: "AgreementParty_agreementId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "AgreementParty_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const agreementParty = pgTable( + 'AgreementParty', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + agreementId: text().notNull(), + residentId: text().notNull(), + acceptedAt: timestamp({ precision: 3, mode: 'date' }), + declinedAt: timestamp({ precision: 3, mode: 'date' }), + }, + (table) => [ + uniqueIndex('AgreementParty_agreementId_residentId_key').using( + 'btree', + table.agreementId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + ), + index('AgreementParty_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.agreementId], + foreignColumns: [conflictAgreement.id], + name: 'AgreementParty_agreementId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'AgreementParty_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const residentPhoto = pgTable("ResidentPhoto", { - residentId: text().primaryKey().notNull(), - data: bytea().notNull(), - mimeType: text().notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), -}, (table) => [ - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "ResidentPhoto_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const residentPhoto = pgTable( + 'ResidentPhoto', + { + residentId: text().primaryKey().notNull(), + data: bytea().notNull(), + mimeType: text().notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'ResidentPhoto_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const expense = pgTable("Expense", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - paidById: text().notNull(), - createdById: text().notNull(), - description: text().notNull(), - category: text().notNull(), - amountRappen: integer().notNull(), - date: timestamp({ precision: 3, mode: 'date' }).notNull(), -}, (table) => [ - index("Expense_housingUnitId_date_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.date.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "Expense_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.paidById], - foreignColumns: [resident.id], - name: "Expense_paidById_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.createdById], - foreignColumns: [resident.id], - name: "Expense_createdById_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const expense = pgTable( + 'Expense', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + paidById: text().notNull(), + createdById: text().notNull(), + description: text().notNull(), + category: text().notNull(), + amountRappen: integer().notNull(), + date: timestamp({ precision: 3, mode: 'date' }).notNull(), + }, + (table) => [ + index('Expense_housingUnitId_date_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.date.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'Expense_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.paidById], + foreignColumns: [resident.id], + name: 'Expense_paidById_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.createdById], + foreignColumns: [resident.id], + name: 'Expense_createdById_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const expenseShare = pgTable("ExpenseShare", { - id: text().primaryKey().$defaultFn(createId).notNull(), - expenseId: text().notNull(), - residentId: text().notNull(), - amountRappen: integer().notNull(), -}, (table) => [ - uniqueIndex("ExpenseShare_expenseId_residentId_key").using("btree", table.expenseId.asc().nullsLast(), table.residentId.asc().nullsLast()), - index("ExpenseShare_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - foreignKey({ - columns: [table.expenseId], - foreignColumns: [expense.id], - name: "ExpenseShare_expenseId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "ExpenseShare_residentId_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const expenseShare = pgTable( + 'ExpenseShare', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + expenseId: text().notNull(), + residentId: text().notNull(), + amountRappen: integer().notNull(), + }, + (table) => [ + uniqueIndex('ExpenseShare_expenseId_residentId_key').using( + 'btree', + table.expenseId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + ), + index('ExpenseShare_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + foreignKey({ + columns: [table.expenseId], + foreignColumns: [expense.id], + name: 'ExpenseShare_expenseId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'ExpenseShare_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const settlement = pgTable("Settlement", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - housingUnitId: text().notNull(), - fromId: text().notNull(), - toId: text().notNull(), - amountRappen: integer().notNull(), - note: text(), -}, (table) => [ - index("Settlement_housingUnitId_idx").using("btree", table.housingUnitId.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "Settlement_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.fromId], - foreignColumns: [resident.id], - name: "Settlement_fromId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.toId], - foreignColumns: [resident.id], - name: "Settlement_toId_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const settlement = pgTable( + 'Settlement', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + housingUnitId: text().notNull(), + fromId: text().notNull(), + toId: text().notNull(), + amountRappen: integer().notNull(), + note: text(), + }, + (table) => [ + index('Settlement_housingUnitId_idx').using('btree', table.housingUnitId.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'Settlement_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.fromId], + foreignColumns: [resident.id], + name: 'Settlement_fromId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.toId], + foreignColumns: [resident.id], + name: 'Settlement_toId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const account = pgTable("Account", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - email: text().notNull(), - passwordHash: text(), - emailVerifiedAt: timestamp({ precision: 3, mode: 'date' }), - userId: text(), - residentId: text(), -}, (table) => [ - uniqueIndex("Account_email_key").using("btree", table.email.asc().nullsLast()), - index("Account_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - uniqueIndex("Account_residentId_key").using("btree", table.residentId.asc().nullsLast()), - index("Account_userId_idx").using("btree", table.userId.asc().nullsLast()), - uniqueIndex("Account_userId_key").using("btree", table.userId.asc().nullsLast()), - foreignKey({ - columns: [table.userId], - foreignColumns: [user.id], - name: "Account_userId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "Account_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const account = pgTable( + 'Account', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + email: text().notNull(), + passwordHash: text(), + emailVerifiedAt: timestamp({ precision: 3, mode: 'date' }), + userId: text(), + residentId: text(), + }, + (table) => [ + uniqueIndex('Account_email_key').using('btree', table.email.asc().nullsLast()), + index('Account_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + uniqueIndex('Account_residentId_key').using('btree', table.residentId.asc().nullsLast()), + index('Account_userId_idx').using('btree', table.userId.asc().nullsLast()), + uniqueIndex('Account_userId_key').using('btree', table.userId.asc().nullsLast()), + foreignKey({ + columns: [table.userId], + foreignColumns: [user.id], + name: 'Account_userId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'Account_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const authToken = pgTable("AuthToken", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - tokenHash: text().notNull(), - purpose: authTokenPurpose().notNull(), - expiresAt: timestamp({ precision: 3, mode: 'date' }).notNull(), - usedAt: timestamp({ precision: 3, mode: 'date' }), - accountId: text().notNull(), -}, (table) => [ - index("AuthToken_accountId_purpose_idx").using("btree", table.accountId.asc().nullsLast(), table.purpose.asc().nullsLast()), - uniqueIndex("AuthToken_tokenHash_key").using("btree", table.tokenHash.asc().nullsLast()), - foreignKey({ - columns: [table.accountId], - foreignColumns: [account.id], - name: "AuthToken_accountId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const authToken = pgTable( + 'AuthToken', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + tokenHash: text().notNull(), + purpose: authTokenPurpose().notNull(), + expiresAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + usedAt: timestamp({ precision: 3, mode: 'date' }), + accountId: text().notNull(), + }, + (table) => [ + index('AuthToken_accountId_purpose_idx').using( + 'btree', + table.accountId.asc().nullsLast(), + table.purpose.asc().nullsLast(), + ), + uniqueIndex('AuthToken_tokenHash_key').using('btree', table.tokenHash.asc().nullsLast()), + foreignKey({ + columns: [table.accountId], + foreignColumns: [account.id], + name: 'AuthToken_accountId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const taskCompletion = pgTable("TaskCompletion", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - taskId: text().notNull(), - completedById: text().notNull(), - completedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - notes: text(), - durationMinutes: integer(), - completedItems: text().array().default(sql`ARRAY[]::TEXT[]`), -}, (table) => [ - index("TaskCompletion_completedById_idx").using("btree", table.completedById.asc().nullsLast()), - index("TaskCompletion_taskId_idx").using("btree", table.taskId.asc().nullsLast()), - foreignKey({ - columns: [table.taskId], - foreignColumns: [householdTask.id], - name: "TaskCompletion_taskId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.completedById], - foreignColumns: [resident.id], - name: "TaskCompletion_completedById_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const taskCompletion = pgTable( + 'TaskCompletion', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + taskId: text().notNull(), + completedById: text().notNull(), + completedAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + notes: text(), + durationMinutes: integer(), + completedItems: text() + .array() + .default(sql`ARRAY[]::TEXT[]`), + }, + (table) => [ + index('TaskCompletion_completedById_idx').using('btree', table.completedById.asc().nullsLast()), + index('TaskCompletion_taskId_idx').using('btree', table.taskId.asc().nullsLast()), + foreignKey({ + columns: [table.taskId], + foreignColumns: [householdTask.id], + name: 'TaskCompletion_taskId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.completedById], + foreignColumns: [resident.id], + name: 'TaskCompletion_completedById_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const learningRecord = pgTable("LearningRecord", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - kind: learningKind().notNull(), - title: text().notNull(), - status: learningStatus().default('PLANNED').notNull(), - languageCode: text(), - cefrLevel: text(), - provider: text(), - category: text(), - hours: integer(), - startedAt: timestamp({ precision: 3, mode: 'date' }), - completedAt: timestamp({ precision: 3, mode: 'date' }), - notes: text(), - recordedBy: residentOrStaff().notNull(), -}, (table) => [ - index("LearningRecord_languageCode_cefrLevel_idx").using("btree", table.languageCode.asc().nullsLast(), table.cefrLevel.asc().nullsLast()), - index("LearningRecord_residentId_kind_idx").using("btree", table.residentId.asc().nullsLast(), table.kind.asc().nullsLast()), - index("LearningRecord_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "LearningRecord_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const learningRecord = pgTable( + 'LearningRecord', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + kind: learningKind().notNull(), + title: text().notNull(), + status: learningStatus().default('PLANNED').notNull(), + languageCode: text(), + cefrLevel: text(), + provider: text(), + category: text(), + hours: integer(), + startedAt: timestamp({ precision: 3, mode: 'date' }), + completedAt: timestamp({ precision: 3, mode: 'date' }), + notes: text(), + recordedBy: residentOrStaff().notNull(), + }, + (table) => [ + index('LearningRecord_languageCode_cefrLevel_idx').using( + 'btree', + table.languageCode.asc().nullsLast(), + table.cefrLevel.asc().nullsLast(), + ), + index('LearningRecord_residentId_kind_idx').using( + 'btree', + table.residentId.asc().nullsLast(), + table.kind.asc().nullsLast(), + ), + index('LearningRecord_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'LearningRecord_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const messageThread = pgTable("MessageThread", { - id: text().primaryKey().$defaultFn(createId).notNull(), - residentId: text().notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), -}, (table) => [ - uniqueIndex("MessageThread_residentId_key").using("btree", table.residentId.asc().nullsLast()), - index("MessageThread_updatedAt_idx").using("btree", table.updatedAt.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "MessageThread_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const messageThread = pgTable( + 'MessageThread', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + residentId: text().notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + }, + (table) => [ + uniqueIndex('MessageThread_residentId_key').using('btree', table.residentId.asc().nullsLast()), + index('MessageThread_updatedAt_idx').using('btree', table.updatedAt.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'MessageThread_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const message = pgTable("Message", { - id: text().primaryKey().$defaultFn(createId).notNull(), - threadId: text().notNull(), - authorResidentId: text(), - authorUserId: text(), - body: text().notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - readAt: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - index("Message_threadId_createdAt_idx").using("btree", table.threadId.asc().nullsLast(), table.createdAt.asc().nullsLast()), - foreignKey({ - columns: [table.threadId], - foreignColumns: [messageThread.id], - name: "Message_threadId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.authorResidentId], - foreignColumns: [resident.id], - name: "Message_authorResidentId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.authorUserId], - foreignColumns: [user.id], - name: "Message_authorUserId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - check("Message_one_author", sql`("authorResidentId" IS NOT NULL) <> ("authorUserId" IS NOT NULL)`), -]); +export const message = pgTable( + 'Message', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + threadId: text().notNull(), + authorResidentId: text(), + authorUserId: text(), + body: text().notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + readAt: timestamp({ precision: 3, mode: 'date' }), + }, + (table) => [ + index('Message_threadId_createdAt_idx').using( + 'btree', + table.threadId.asc().nullsLast(), + table.createdAt.asc().nullsLast(), + ), + foreignKey({ + columns: [table.threadId], + foreignColumns: [messageThread.id], + name: 'Message_threadId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.authorResidentId], + foreignColumns: [resident.id], + name: 'Message_authorResidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.authorUserId], + foreignColumns: [user.id], + name: 'Message_authorUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + check( + 'Message_one_author', + sql`("authorResidentId" IS NOT NULL) <> ("authorUserId" IS NOT NULL)`, + ), + ], +) -export const careAssignment = pgTable("CareAssignment", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - staffId: text().notNull(), - role: careRole().notNull(), -}, (table) => [ - uniqueIndex("CareAssignment_residentId_role_key").using("btree", table.residentId.asc().nullsLast(), table.role.asc().nullsLast()), - index("CareAssignment_staffId_idx").using("btree", table.staffId.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "CareAssignment_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.staffId], - foreignColumns: [user.id], - name: "CareAssignment_staffId_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const careAssignment = pgTable( + 'CareAssignment', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + staffId: text().notNull(), + role: careRole().notNull(), + }, + (table) => [ + uniqueIndex('CareAssignment_residentId_role_key').using( + 'btree', + table.residentId.asc().nullsLast(), + table.role.asc().nullsLast(), + ), + index('CareAssignment_staffId_idx').using('btree', table.staffId.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'CareAssignment_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.staffId], + foreignColumns: [user.id], + name: 'CareAssignment_staffId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const careAttribute = pgTable("CareAttribute", { - id: text().primaryKey().$defaultFn(createId).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - domain: careRole().notNull(), - key: text().notNull(), - value: text().notNull(), - updatedById: text().notNull(), -}, (table) => [ - index("CareAttribute_residentId_domain_idx").using("btree", table.residentId.asc().nullsLast(), table.domain.asc().nullsLast()), - uniqueIndex("CareAttribute_residentId_domain_key_key").using("btree", table.residentId.asc().nullsLast(), table.domain.asc().nullsLast(), table.key.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "CareAttribute_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.updatedById], - foreignColumns: [user.id], - name: "CareAttribute_updatedById_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const careAttribute = pgTable( + 'CareAttribute', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + domain: careRole().notNull(), + key: text().notNull(), + value: text().notNull(), + updatedById: text().notNull(), + }, + (table) => [ + index('CareAttribute_residentId_domain_idx').using( + 'btree', + table.residentId.asc().nullsLast(), + table.domain.asc().nullsLast(), + ), + uniqueIndex('CareAttribute_residentId_domain_key_key').using( + 'btree', + table.residentId.asc().nullsLast(), + table.domain.asc().nullsLast(), + table.key.asc().nullsLast(), + ), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'CareAttribute_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.updatedById], + foreignColumns: [user.id], + name: 'CareAttribute_updatedById_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const houseEvent = pgTable("HouseEvent", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - title: text().notNull(), - description: text().notNull(), - category: houseEventCategory().default('SOCIAL').notNull(), - location: text(), - startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), - endsAt: timestamp({ precision: 3, mode: 'date' }), - status: houseEventStatus().default('PUBLISHED').notNull(), - createdByStaffId: text(), - createdByResidentId: text(), -}, (table) => [ - index("HouseEvent_housingUnitId_startsAt_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.startsAt.asc().nullsLast()), - index("HouseEvent_status_startsAt_idx").using("btree", table.status.asc().nullsLast(), table.startsAt.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "HouseEvent_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.createdByStaffId], - foreignColumns: [user.id], - name: "HouseEvent_createdByStaffId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.createdByResidentId], - foreignColumns: [resident.id], - name: "HouseEvent_createdByResidentId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const houseEvent = pgTable( + 'HouseEvent', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + title: text().notNull(), + description: text().notNull(), + category: houseEventCategory().default('SOCIAL').notNull(), + location: text(), + startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + endsAt: timestamp({ precision: 3, mode: 'date' }), + status: houseEventStatus().default('PUBLISHED').notNull(), + createdByStaffId: text(), + createdByResidentId: text(), + }, + (table) => [ + index('HouseEvent_housingUnitId_startsAt_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.startsAt.asc().nullsLast(), + ), + index('HouseEvent_status_startsAt_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.startsAt.asc().nullsLast(), + ), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'HouseEvent_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.createdByStaffId], + foreignColumns: [user.id], + name: 'HouseEvent_createdByStaffId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.createdByResidentId], + foreignColumns: [resident.id], + name: 'HouseEvent_createdByResidentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const appointment = pgTable("Appointment", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - staffId: text(), - domain: careRole().notNull(), - title: text().notNull(), - startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), - endsAt: timestamp({ precision: 3, mode: 'date' }), - location: text(), - notes: text(), - status: appointmentStatus().default('SCHEDULED').notNull(), - residentNote: text(), - staffNote: text(), -}, (table) => [ - index("Appointment_residentId_startsAt_idx").using("btree", table.residentId.asc().nullsLast(), table.startsAt.asc().nullsLast()), - index("Appointment_staffId_startsAt_idx").using("btree", table.staffId.asc().nullsLast(), table.startsAt.asc().nullsLast()), - index("Appointment_status_domain_idx").using("btree", table.status.asc().nullsLast(), table.domain.asc().nullsLast()), - index("Appointment_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "Appointment_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.staffId], - foreignColumns: [user.id], - name: "Appointment_staffId_fkey" - }).onUpdate("cascade").onDelete("restrict"), -]); +export const appointment = pgTable( + 'Appointment', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + staffId: text(), + domain: careRole().notNull(), + title: text().notNull(), + startsAt: timestamp({ precision: 3, mode: 'date' }).notNull(), + endsAt: timestamp({ precision: 3, mode: 'date' }), + location: text(), + notes: text(), + status: appointmentStatus().default('SCHEDULED').notNull(), + residentNote: text(), + staffNote: text(), + }, + (table) => [ + index('Appointment_residentId_startsAt_idx').using( + 'btree', + table.residentId.asc().nullsLast(), + table.startsAt.asc().nullsLast(), + ), + index('Appointment_staffId_startsAt_idx').using( + 'btree', + table.staffId.asc().nullsLast(), + table.startsAt.asc().nullsLast(), + ), + index('Appointment_status_domain_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.domain.asc().nullsLast(), + ), + index('Appointment_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'Appointment_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.staffId], + foreignColumns: [user.id], + name: 'Appointment_staffId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + ], +) -export const eventRsvp = pgTable("EventRsvp", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - eventId: text().notNull(), - residentId: text().notNull(), - status: eventRsvpStatus().default('GOING').notNull(), -}, (table) => [ - index("EventRsvp_eventId_idx").using("btree", table.eventId.asc().nullsLast()), - uniqueIndex("EventRsvp_eventId_residentId_key").using("btree", table.eventId.asc().nullsLast(), table.residentId.asc().nullsLast()), - foreignKey({ - columns: [table.eventId], - foreignColumns: [houseEvent.id], - name: "EventRsvp_eventId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "EventRsvp_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const eventRsvp = pgTable( + 'EventRsvp', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + eventId: text().notNull(), + residentId: text().notNull(), + status: eventRsvpStatus().default('GOING').notNull(), + }, + (table) => [ + index('EventRsvp_eventId_idx').using('btree', table.eventId.asc().nullsLast()), + uniqueIndex('EventRsvp_eventId_residentId_key').using( + 'btree', + table.eventId.asc().nullsLast(), + table.residentId.asc().nullsLast(), + ), + foreignKey({ + columns: [table.eventId], + foreignColumns: [houseEvent.id], + name: 'EventRsvp_eventId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'EventRsvp_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) -export const opportunity = pgTable("Opportunity", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - kind: opportunityKind().notNull(), - title: text().notNull(), - description: text().notNull(), - organisation: text().notNull(), - location: text(), - schedule: text(), - hoursPerWeek: integer(), - seats: integer(), - germanLevel: text(), - permitRequirement: permitRequirement().default('NONE').notNull(), - requirementNote: text(), - contactName: text(), - contactEmail: text(), - contactPhone: text(), - website: text(), - status: opportunityStatus().default('DRAFT').notNull(), - startsAt: timestamp({ precision: 3, mode: 'date' }), - endsAt: timestamp({ precision: 3, mode: 'date' }), - createdByUserId: text(), - updatedByUserId: text(), -}, (table) => [ - index("Opportunity_endsAt_idx").using("btree", table.endsAt.asc().nullsLast()), - index("Opportunity_status_kind_idx").using("btree", table.status.asc().nullsLast(), table.kind.asc().nullsLast()), - foreignKey({ - columns: [table.createdByUserId], - foreignColumns: [user.id], - name: "Opportunity_createdByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.updatedByUserId], - foreignColumns: [user.id], - name: "Opportunity_updatedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const opportunity = pgTable( + 'Opportunity', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + kind: opportunityKind().notNull(), + title: text().notNull(), + description: text().notNull(), + organisation: text().notNull(), + location: text(), + schedule: text(), + hoursPerWeek: integer(), + seats: integer(), + germanLevel: text(), + permitRequirement: permitRequirement().default('NONE').notNull(), + requirementNote: text(), + contactName: text(), + contactEmail: text(), + contactPhone: text(), + website: text(), + status: opportunityStatus().default('DRAFT').notNull(), + startsAt: timestamp({ precision: 3, mode: 'date' }), + endsAt: timestamp({ precision: 3, mode: 'date' }), + createdByUserId: text(), + updatedByUserId: text(), + }, + (table) => [ + index('Opportunity_endsAt_idx').using('btree', table.endsAt.asc().nullsLast()), + index('Opportunity_status_kind_idx').using( + 'btree', + table.status.asc().nullsLast(), + table.kind.asc().nullsLast(), + ), + foreignKey({ + columns: [table.createdByUserId], + foreignColumns: [user.id], + name: 'Opportunity_createdByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.updatedByUserId], + foreignColumns: [user.id], + name: 'Opportunity_updatedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const opportunityApplication = pgTable("OpportunityApplication", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - opportunityId: text().notNull(), - stage: applicationStage().default('INTERESTED').notNull(), - stageChangedAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - note: text(), - createdBy: residentOrStaff().notNull(), - supportedByUserId: text(), - learningRecordId: text(), -}, (table) => [ - uniqueIndex("OpportunityApplication_learningRecordId_key").using("btree", table.learningRecordId.asc().nullsLast()), - index("OpportunityApplication_opportunityId_stage_idx").using("btree", table.opportunityId.asc().nullsLast(), table.stage.asc().nullsLast()), - index("OpportunityApplication_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - uniqueIndex("OpportunityApplication_residentId_opportunityId_key").using("btree", table.residentId.asc().nullsLast(), table.opportunityId.asc().nullsLast()), - index("OpportunityApplication_stage_idx").using("btree", table.stage.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "OpportunityApplication_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.opportunityId], - foreignColumns: [opportunity.id], - name: "OpportunityApplication_opportunityId_fkey" - }).onUpdate("cascade").onDelete("restrict"), - foreignKey({ - columns: [table.supportedByUserId], - foreignColumns: [user.id], - name: "OpportunityApplication_supportedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.learningRecordId], - foreignColumns: [learningRecord.id], - name: "OpportunityApplication_learningRecordId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const opportunityApplication = pgTable( + 'OpportunityApplication', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + opportunityId: text().notNull(), + stage: applicationStage().default('INTERESTED').notNull(), + stageChangedAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + note: text(), + createdBy: residentOrStaff().notNull(), + supportedByUserId: text(), + learningRecordId: text(), + }, + (table) => [ + uniqueIndex('OpportunityApplication_learningRecordId_key').using( + 'btree', + table.learningRecordId.asc().nullsLast(), + ), + index('OpportunityApplication_opportunityId_stage_idx').using( + 'btree', + table.opportunityId.asc().nullsLast(), + table.stage.asc().nullsLast(), + ), + index('OpportunityApplication_residentId_idx').using( + 'btree', + table.residentId.asc().nullsLast(), + ), + uniqueIndex('OpportunityApplication_residentId_opportunityId_key').using( + 'btree', + table.residentId.asc().nullsLast(), + table.opportunityId.asc().nullsLast(), + ), + index('OpportunityApplication_stage_idx').using('btree', table.stage.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'OpportunityApplication_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.opportunityId], + foreignColumns: [opportunity.id], + name: 'OpportunityApplication_opportunityId_fkey', + }) + .onUpdate('cascade') + .onDelete('restrict'), + foreignKey({ + columns: [table.supportedByUserId], + foreignColumns: [user.id], + name: 'OpportunityApplication_supportedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.learningRecordId], + foreignColumns: [learningRecord.id], + name: 'OpportunityApplication_learningRecordId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const marketplacePost = pgTable("MarketplacePost", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - housingUnitId: text().notNull(), - postedById: text().notNull(), - title: text().notNull(), - description: text().notNull(), - kind: marketplacePostKind().notNull(), - category: text().default('OTHER').notNull(), - status: marketplacePostStatus().default('OPEN').notNull(), - claimedById: text(), - closedAt: timestamp({ precision: 3, mode: 'date' }), - hiddenByStaff: boolean().default(false).notNull(), - hiddenReason: text(), - contactNote: text(), - claimedAt: timestamp({ precision: 3, mode: 'date' }), -}, (table) => [ - index("MarketplacePost_housingUnitId_status_idx").using("btree", table.housingUnitId.asc().nullsLast(), table.status.asc().nullsLast()), - index("MarketplacePost_postedById_idx").using("btree", table.postedById.asc().nullsLast()), - foreignKey({ - columns: [table.housingUnitId], - foreignColumns: [housingUnit.id], - name: "MarketplacePost_housingUnitId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.postedById], - foreignColumns: [resident.id], - name: "MarketplacePost_postedById_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.claimedById], - foreignColumns: [resident.id], - name: "MarketplacePost_claimedById_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const marketplacePost = pgTable( + 'MarketplacePost', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + housingUnitId: text().notNull(), + postedById: text().notNull(), + title: text().notNull(), + description: text().notNull(), + kind: marketplacePostKind().notNull(), + category: text().default('OTHER').notNull(), + status: marketplacePostStatus().default('OPEN').notNull(), + claimedById: text(), + closedAt: timestamp({ precision: 3, mode: 'date' }), + hiddenByStaff: boolean().default(false).notNull(), + hiddenReason: text(), + contactNote: text(), + claimedAt: timestamp({ precision: 3, mode: 'date' }), + }, + (table) => [ + index('MarketplacePost_housingUnitId_status_idx').using( + 'btree', + table.housingUnitId.asc().nullsLast(), + table.status.asc().nullsLast(), + ), + index('MarketplacePost_postedById_idx').using('btree', table.postedById.asc().nullsLast()), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'MarketplacePost_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.postedById], + foreignColumns: [resident.id], + name: 'MarketplacePost_postedById_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.claimedById], + foreignColumns: [resident.id], + name: 'MarketplacePost_claimedById_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const complaint = pgTable("Complaint", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text(), - subject: complaintSubject().notNull(), - body: text().notNull(), - status: complaintStatus().default('OPEN').notNull(), - response: text(), - respondedAt: timestamp({ precision: 3, mode: 'date' }), - respondedByUserId: text(), -}, (table) => [ - index("Complaint_createdAt_idx").using("btree", table.createdAt.asc().nullsLast()), - index("Complaint_residentId_idx").using("btree", table.residentId.asc().nullsLast()), - index("Complaint_status_idx").using("btree", table.status.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "Complaint_residentId_fkey" - }).onUpdate("cascade").onDelete("set null"), - foreignKey({ - columns: [table.respondedByUserId], - foreignColumns: [user.id], - name: "Complaint_respondedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const complaint = pgTable( + 'Complaint', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text(), + subject: complaintSubject().notNull(), + body: text().notNull(), + status: complaintStatus().default('OPEN').notNull(), + response: text(), + respondedAt: timestamp({ precision: 3, mode: 'date' }), + respondedByUserId: text(), + }, + (table) => [ + index('Complaint_createdAt_idx').using('btree', table.createdAt.asc().nullsLast()), + index('Complaint_residentId_idx').using('btree', table.residentId.asc().nullsLast()), + index('Complaint_status_idx').using('btree', table.status.asc().nullsLast()), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'Complaint_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + foreignKey({ + columns: [table.respondedByUserId], + foreignColumns: [user.id], + name: 'Complaint_respondedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const residentDocument = pgTable("ResidentDocument", { - id: text().primaryKey().$defaultFn(createId).notNull(), - createdAt: timestamp({ precision: 3, mode: 'date' }).default(sql`CURRENT_TIMESTAMP`).notNull(), - updatedAt: timestamp({ precision: 3, mode: 'date' }) - .$defaultFn(() => new Date()) - .$onUpdateFn(() => new Date()) - .notNull(), - residentId: text().notNull(), - category: text().default('OTHER').notNull(), - title: text().notNull(), - fileName: text().notNull(), - mimeType: text().notNull(), - sizeBytes: integer().notNull(), - uploadedByUserId: text(), -}, (table) => [ - index("ResidentDocument_residentId_createdAt_idx").using("btree", table.residentId.asc().nullsLast(), table.createdAt.asc().nullsLast()), - foreignKey({ - columns: [table.residentId], - foreignColumns: [resident.id], - name: "ResidentDocument_residentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), - foreignKey({ - columns: [table.uploadedByUserId], - foreignColumns: [user.id], - name: "ResidentDocument_uploadedByUserId_fkey" - }).onUpdate("cascade").onDelete("set null"), -]); +export const residentDocument = pgTable( + 'ResidentDocument', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp({ precision: 3, mode: 'date' }) + .$defaultFn(() => new Date()) + .$onUpdateFn(() => new Date()) + .notNull(), + residentId: text().notNull(), + category: text().default('OTHER').notNull(), + title: text().notNull(), + fileName: text().notNull(), + mimeType: text().notNull(), + sizeBytes: integer().notNull(), + uploadedByUserId: text(), + }, + (table) => [ + index('ResidentDocument_residentId_createdAt_idx').using( + 'btree', + table.residentId.asc().nullsLast(), + table.createdAt.asc().nullsLast(), + ), + foreignKey({ + columns: [table.residentId], + foreignColumns: [resident.id], + name: 'ResidentDocument_residentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.uploadedByUserId], + foreignColumns: [user.id], + name: 'ResidentDocument_uploadedByUserId_fkey', + }) + .onUpdate('cascade') + .onDelete('set null'), + ], +) -export const residentDocumentBlob = pgTable("ResidentDocumentBlob", { - documentId: text().primaryKey().notNull(), - data: bytea().notNull(), -}, (table) => [ - foreignKey({ - columns: [table.documentId], - foreignColumns: [residentDocument.id], - name: "ResidentDocumentBlob_documentId_fkey" - }).onUpdate("cascade").onDelete("cascade"), -]); +export const residentDocumentBlob = pgTable( + 'ResidentDocumentBlob', + { + documentId: text().primaryKey().notNull(), + data: bytea().notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.documentId], + foreignColumns: [residentDocument.id], + name: 'ResidentDocumentBlob_documentId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) diff --git a/src/lib/demo/reset.ts b/src/lib/demo/reset.ts index 6870c1ec..16666ca1 100644 --- a/src/lib/demo/reset.ts +++ b/src/lib/demo/reset.ts @@ -19,7 +19,8 @@ * through ts-node, which does not resolve tsconfig path aliases. */ -import type { PrismaClient } from '@prisma/client' +import { asc } from 'drizzle-orm' +import { resident, type db } from '../db' import { seedDemoData, type DemoSeedSummary } from './seed-data' import { syncOrgRules } from '../governance/sync-org-rules' import { upsertDemoStaff, upsertDemoStaffRoles } from './staff' @@ -34,17 +35,17 @@ export interface DemoResetSummary extends DemoSeedSummary { opportunityApplications: number } -export async function resetDemoData(prisma: PrismaClient): Promise { - const tablesWiped = await wipeAllExceptKeepList(prisma) +export async function resetDemoData(dbClient: typeof db): Promise { + const tablesWiped = await wipeAllExceptKeepList(dbClient) // BEFORE the seed, not after: the seed hands this account the care seats on // every demo resident, and an assignment cannot point at a row that does not // exist yet. (The wipe keeps User, so this is an update on a repeat run.) - const demoStaff = await upsertDemoStaff(prisma) + const demoStaff = await upsertDemoStaff(dbClient) // Every role door, so the visitor can walk the product as each of them. - await upsertDemoStaffRoles(prisma) + await upsertDemoStaffRoles(dbClient) - const seeded = await seedDemoData(prisma, { + const seeded = await seedDemoData(dbClient, { careStaffId: demoStaff?.id ?? null, // Full scope owns the whole database, so it can also own — and next time // truncate — content that no demo prefix reaches. @@ -55,16 +56,16 @@ export async function resetDemoData(prisma: PrismaClient): Promise resident.id), staffId: demoStaff?.id ?? null, }) - await syncOrgRules(prisma) + await syncOrgRules(dbClient) return { ...seeded, diff --git a/src/lib/demo/scoped-reset.ts b/src/lib/demo/scoped-reset.ts index ccf1204e..48fbfd19 100644 --- a/src/lib/demo/scoped-reset.ts +++ b/src/lib/demo/scoped-reset.ts @@ -17,7 +17,17 @@ * Relative-import-safe (no '@/' aliases): loaded through ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { eq, inArray, like, or } from 'drizzle-orm' +import { + escapeLike, + housingUnit, + incident, + message, + messageThread, + placement, + resident, + type db, +} from '../db' import { resolveDemoResidentCode, ALL_DEMO_RESIDENT_CODE_PREFIXES, @@ -34,24 +44,32 @@ export interface DemoWorldResetSummary extends DemoSeedSummary { } /** Delete every demo unit and demo resident. No-op when absent. */ -export async function deleteDemoWorld(prisma: PrismaClient): Promise<{ +export async function deleteDemoWorld(dbClient: typeof db): Promise<{ unitsDeleted: number residentsDeleted: number }> { - const demoUnitFilter = { code: { startsWith: DEMO_UNIT_CODE_PREFIX } } + const demoUnitFilter = like(housingUnit.code, `${escapeLike(DEMO_UNIT_CODE_PREFIX)}%`) // Every demo prefix ever issued, not just this brand's: a demo resident // seeded under a previous client prefix would otherwise survive every reset, // uncleanable and — on a `unit`-scope instance — parked next to real data. - const demoResidentFilter = { - OR: [ - ...ALL_DEMO_RESIDENT_CODE_PREFIXES.map((prefix) => ({ code: { startsWith: prefix } })), - { code: resolveDemoResidentCode() }, - ], - } + const demoResidentFilter = or( + ...ALL_DEMO_RESIDENT_CODE_PREFIXES.map((prefix) => + like(resident.code, `${escapeLike(prefix)}%`), + ), + eq(resident.code, resolveDemoResidentCode()), + ) + const demoUnitIds = dbClient + .select({ id: housingUnit.id }) + .from(housingUnit) + .where(demoUnitFilter) + const demoResidentIds = dbClient + .select({ id: resident.id }) + .from(resident) + .where(demoResidentFilter) - await prisma.incident.deleteMany({ where: { housingUnit: demoUnitFilter } }) - await prisma.placement.deleteMany({ where: { housingUnit: demoUnitFilter } }) - const units = await prisma.housingUnit.deleteMany({ where: demoUnitFilter }) + await dbClient.delete(incident).where(inArray(incident.housingUnitId, demoUnitIds)) + await dbClient.delete(placement).where(inArray(placement.housingUnitId, demoUnitIds)) + const units = await dbClient.delete(housingUnit).where(demoUnitFilter) // Messages a demo resident WROTE hold a Restrict foreign key, so they veto // the resident delete below. Restrict is right for real data — nobody should @@ -62,21 +80,21 @@ export async function deleteDemoWorld(prisma: PrismaClient): Promise<{ // Postgres reports only the FIRST blocking foreign key, so a missing delete // here does not surface as "you forgot messages"; it surfaces as the whole // nightly reset failing, and the demo silently rotting from that day on. - await prisma.message.deleteMany({ where: { authorResident: demoResidentFilter } }) - await prisma.messageThread.deleteMany({ where: { resident: demoResidentFilter } }) + await dbClient.delete(message).where(inArray(message.authorResidentId, demoResidentIds)) + await dbClient.delete(messageThread).where(inArray(messageThread.residentId, demoResidentIds)) - const residents = await prisma.resident.deleteMany({ where: demoResidentFilter }) + const residents = await dbClient.delete(resident).where(demoResidentFilter) - return { unitsDeleted: units.count, residentsDeleted: residents.count } + return { unitsDeleted: units.rowCount ?? 0, residentsDeleted: residents.rowCount ?? 0 } } /** Tear down and reseed the demo world; self-heal the demo staff account. */ -export async function resetDemoWorld(prisma: PrismaClient): Promise { - const removed = await deleteDemoWorld(prisma) +export async function resetDemoWorld(dbClient: typeof db): Promise { + const removed = await deleteDemoWorld(dbClient) // The AOZ catalog is reference data, not demo data — it is never deleted // above. But the demo's adopted house rule points at an ORG rule by key, so // the catalog has to be present before seeding, not merely usually present. - await syncOrgRules(prisma) + await syncOrgRules(dbClient) // Before the seed: it assigns this account the care seats on every demo // resident, so the account has to exist first. // Deliberately the SINGLE configured door, not the per-role set. This scope @@ -84,8 +102,8 @@ export async function resetDemoWorld(prisma: PrismaClient): Promise { // The portal demo logs in as Fatima: PLACED, in the zero-conflict success @@ -77,8 +90,9 @@ export async function seedDemoData( // ======================================================================== // SUCCESS UNIT (Unit 5) - 4 highly compatible residents - const fatima = await prisma.resident.create({ - data: { + const [fatima] = await dbClient + .insert(resident) + .values({ code: demoResidentCode, // Self-chosen profile — shows the resident-profile feature in the tour. displayName: 'Fatima', @@ -111,11 +125,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const yasmin = await prisma.resident.create({ - data: { + const [yasmin] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}02`, displayName: 'Yasmin', ageRange: 'ADULT', @@ -146,11 +161,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const amira = await prisma.resident.create({ - data: { + const [amira] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}03`, displayName: 'Amira', ageRange: 'YOUNG_ADULT', @@ -181,11 +197,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const sara = await prisma.resident.create({ - data: { + const [sara] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}04`, displayName: 'Sara', ageRange: 'ADULT', @@ -216,12 +233,13 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() // PROBLEM UNIT (Unit 12) - 4 incompatible residents - const marco = await prisma.resident.create({ - data: { + const [marco] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}05`, displayName: 'Marco', ageRange: 'YOUNG_ADULT', @@ -252,11 +270,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const dmitri = await prisma.resident.create({ - data: { + const [dmitri] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}06`, displayName: 'Dmitri', ageRange: 'ADULT', @@ -287,11 +306,12 @@ export async function seedDemoData( supportLevel: 'ELEVATED', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const petro = await prisma.resident.create({ - data: { + const [petro] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}07`, displayName: 'Petro', ageRange: 'YOUNG_ADULT', @@ -322,11 +342,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const alexei = await prisma.resident.create({ - data: { + const [alexei] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}08`, displayName: 'Alexei', ageRange: 'ADULT', @@ -359,12 +380,13 @@ export async function seedDemoData( hasMedicalDocumentation: true, medicalDocType: 'PRIVATE_ROOM', medicalDocDate: new Date('2024-01-15'), - }, - }) + }) + .returning() // UNIT 7 RESIDENTS - Good mid-tier unit - const habib = await prisma.resident.create({ - data: { + const [habib] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}09`, displayName: 'Habib', ageRange: 'ADULT', @@ -395,11 +417,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const omar = await prisma.resident.create({ - data: { + const [omar] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}10`, displayName: 'Omar', ageRange: 'YOUNG_ADULT', @@ -430,11 +453,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const mustafa = await prisma.resident.create({ - data: { + const [mustafa] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}11`, displayName: 'Mustafa', ageRange: 'ADULT', @@ -465,12 +489,13 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() // UNIT 9 RESIDENTS - Mixed unit - const elena = await prisma.resident.create({ - data: { + const [elena] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}12`, displayName: 'Elena', ageRange: 'MIDDLE_AGED', @@ -501,11 +526,12 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const grace = await prisma.resident.create({ - data: { + const [grace] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}13`, displayName: 'Grace', ageRange: 'YOUNG_ADULT', @@ -536,12 +562,13 @@ export async function seedDemoData( supportLevel: 'STANDARD', status: 'PLACED', hasMedicalDocumentation: false, - }, - }) + }) + .returning() // UNPLACED RESIDENTS - The stars of the demo - const ahmed = await prisma.resident.create({ - data: { + const [ahmed] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}14`, displayName: 'Ahmed', ageRange: 'ADULT', @@ -573,11 +600,12 @@ export async function seedDemoData( status: 'ACTIVE', // UNPLACED - This is our demo star! notes: 'New arrival today - needs placement. Good candidate for Arabic-speaking unit.', hasMedicalDocumentation: false, - }, - }) + }) + .returning() - const maria = await prisma.resident.create({ - data: { + const [maria] = await dbClient + .insert(resident) + .values({ code: `${DEMO_RESIDENT_CODE_PREFIX}15`, displayName: 'Maria', ageRange: 'MIDDLE_AGED', @@ -610,16 +638,17 @@ export async function seedDemoData( notes: 'Previously transferred 3 times due to cleanliness conflicts. Needs quiet, clean environment.', hasMedicalDocumentation: false, - }, - }) + }) + .returning() // ======================================================================== // HOUSING UNITS // ======================================================================== // UNIT 5 - THE SUCCESS STORY - const unit5 = await prisma.housingUnit.create({ - data: { + const [unit5] = await dbClient + .insert(housingUnit) + .values({ code: `${DEMO_UNIT_CODE_PREFIX}U05`, address: 'Mühlebachstrasse 45, 8008 Zürich', // Resident-chosen apartment name — shows the apartment profile feature. @@ -643,12 +672,13 @@ export async function seedDemoData( nearSchools: false, status: 'FULL', notes: 'Success story - 6 months with zero conflicts. All residents highly compatible.', - }, - }) + }) + .returning() // UNIT 12 - THE PROBLEM UNIT - const unit12 = await prisma.housingUnit.create({ - data: { + const [unit12] = await dbClient + .insert(housingUnit) + .values({ code: `${DEMO_UNIT_CODE_PREFIX}U12`, address: 'Langstrasse 127, 8004 Zürich', totalBeds: 5, @@ -670,12 +700,13 @@ export async function seedDemoData( nearSchools: false, status: 'AVAILABLE', notes: 'Historical issues with noise and cleanliness. Needs careful matching.', - }, - }) + }) + .returning() // UNIT 7 - GOOD UNIT (ready for Ahmed) - const unit7 = await prisma.housingUnit.create({ - data: { + const [unit7] = await dbClient + .insert(housingUnit) + .values({ code: `${DEMO_UNIT_CODE_PREFIX}U07`, address: 'Badenerstrasse 88, 8004 Zürich', totalBeds: 4, @@ -697,12 +728,13 @@ export async function seedDemoData( nearSchools: false, status: 'AVAILABLE', notes: 'Stable unit with Arabic-speaking residents. Good for cultural integration.', - }, - }) + }) + .returning() // UNIT 3 - EMPTY UNIT - const unit3 = await prisma.housingUnit.create({ - data: { + const [unit3] = await dbClient + .insert(housingUnit) + .values({ code: `${DEMO_UNIT_CODE_PREFIX}U03`, address: 'Hohlstrasse 56, 8004 Zürich', totalBeds: 3, @@ -724,12 +756,13 @@ export async function seedDemoData( nearSchools: true, status: 'AVAILABLE', notes: 'Newly available unit. All private rooms. Ground floor with wheelchair access.', - }, - }) + }) + .returning() // UNIT 9 - MIXED UNIT - const unit9 = await prisma.housingUnit.create({ - data: { + const [unit9] = await dbClient + .insert(housingUnit) + .values({ code: `${DEMO_UNIT_CODE_PREFIX}U09`, address: 'Josefstrasse 34, 8005 Zürich', totalBeds: 3, @@ -751,88 +784,96 @@ export async function seedDemoData( nearSchools: false, status: 'AVAILABLE', notes: 'Mixed demographic unit. Moderate performance.', - }, - }) + }) + .returning() // ======================================================================== // PLACEMENT SPOTS // ======================================================================== // Unit 5 spots (all occupied) - const unit5Bed1 = await prisma.placementSpot.create({ - data: { + const [unit5Bed1] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit5.id, code: 'R1-B1', label: 'Room 1 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit5Bed2 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit5Bed2] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit5.id, code: 'R1-B2', label: 'Room 1 - Bed 2', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit5Bed3 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit5Bed3] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit5.id, code: 'R2-B1', label: 'Room 2 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit5Bed4 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit5Bed4] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit5.id, code: 'R2-B2', label: 'Room 2 - Bed 2', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) + }) + .returning() // Unit 12 spots - const unit12Bed1 = await prisma.placementSpot.create({ - data: { + const [unit12Bed1] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit12.id, code: 'R1-B1', label: 'Room 1 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit12Bed2 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit12Bed2] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit12.id, code: 'R1-B2', label: 'Room 1 - Bed 2', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit12Bed3 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit12Bed3] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit12.id, code: 'R2-B1', label: 'Room 2 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit12Room3 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit12Room3] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit12.id, code: 'R3', label: 'Private Room 3', @@ -840,123 +881,122 @@ export async function seedDemoData( capacity: 1, status: 'OCCUPIED', requiresMedicalDocs: true, - }, - }) - const unit12Bed5 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit12Bed5] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit12.id, code: 'R2-B2', label: 'Room 2 - Bed 2', type: 'BED', capacity: 1, status: 'AVAILABLE', - }, - }) + }) + .returning() // Unit 7 spots (one available for Ahmed!) - const unit7Bed1 = await prisma.placementSpot.create({ - data: { + const [unit7Bed1] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit7.id, code: 'R1-B1', label: 'Room 1 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit7Bed2 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit7Bed2] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit7.id, code: 'R1-B2', label: 'Room 1 - Bed 2', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit7Bed3 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit7Bed3] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit7.id, code: 'R2-B1', label: 'Room 2 - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit7Bed4 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit7Bed4] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit7.id, code: 'R2-B2', label: 'Room 2 - Bed 2', type: 'BED', capacity: 1, status: 'AVAILABLE', - }, - }) + }) + .returning() // Unit 3 spots (all empty) - await prisma.placementSpot.create({ - data: { - housingUnitId: unit3.id, - code: 'R1', - label: 'Private Room 1', - type: 'PRIVATE_ROOM', - capacity: 1, - status: 'AVAILABLE', - }, - }) - await prisma.placementSpot.create({ - data: { - housingUnitId: unit3.id, - code: 'R2', - label: 'Private Room 2', - type: 'PRIVATE_ROOM', - capacity: 1, - status: 'AVAILABLE', - }, - }) - await prisma.placementSpot.create({ - data: { - housingUnitId: unit3.id, - code: 'R3', - label: 'Private Room 3', - type: 'PRIVATE_ROOM', - capacity: 1, - status: 'AVAILABLE', - }, + await dbClient.insert(placementSpot).values({ + housingUnitId: unit3.id, + code: 'R1', + label: 'Private Room 1', + type: 'PRIVATE_ROOM', + capacity: 1, + status: 'AVAILABLE', + }) + await dbClient.insert(placementSpot).values({ + housingUnitId: unit3.id, + code: 'R2', + label: 'Private Room 2', + type: 'PRIVATE_ROOM', + capacity: 1, + status: 'AVAILABLE', + }) + await dbClient.insert(placementSpot).values({ + housingUnitId: unit3.id, + code: 'R3', + label: 'Private Room 3', + type: 'PRIVATE_ROOM', + capacity: 1, + status: 'AVAILABLE', }) // Unit 9 spots - const unit9Bed1 = await prisma.placementSpot.create({ - data: { + const [unit9Bed1] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit9.id, code: 'R1-B1', label: 'Shared Room - Bed 1', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - const unit9Bed2 = await prisma.placementSpot.create({ - data: { + }) + .returning() + const [unit9Bed2] = await dbClient + .insert(placementSpot) + .values({ housingUnitId: unit9.id, code: 'R1-B2', label: 'Shared Room - Bed 2', type: 'BED', capacity: 1, status: 'OCCUPIED', - }, - }) - await prisma.placementSpot.create({ - data: { - housingUnitId: unit9.id, - code: 'R2', - label: 'Private Room', - type: 'PRIVATE_ROOM', - capacity: 1, - status: 'AVAILABLE', - }, + }) + .returning() + await dbClient.insert(placementSpot).values({ + housingUnitId: unit9.id, + code: 'R2', + label: 'Private Room', + type: 'PRIVATE_ROOM', + capacity: 1, + status: 'AVAILABLE', }) // ======================================================================== @@ -973,222 +1013,195 @@ export async function seedDemoData( twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2) // Unit 5 placements (SUCCESS - all high compatibility) - await prisma.placement.create({ - data: { - residentId: fatima.id, - housingUnitId: unit5.id, - spotId: unit5Bed1.id, - startDate: sixMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 88, - lifestyleScore: 85, - socialScore: 92, - practicalScore: 87, - riskScore: 12, - placementNotes: - 'Apartment Fit: 88%\n\nStrong match - similar cultural background and lifestyle preferences.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: yasmin.id, - housingUnitId: unit5.id, - spotId: unit5Bed2.id, - startDate: sixMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 85, - lifestyleScore: 83, - socialScore: 90, - practicalScore: 84, - riskScore: 15, - placementNotes: - 'Apartment Fit: 85%\n\nExcellent language match with Fatima (both Arabic speakers).', - }, - }) - - await prisma.placement.create({ - data: { - residentId: amira.id, - housingUnitId: unit5.id, - spotId: unit5Bed3.id, - startDate: sixMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 82, - lifestyleScore: 80, - socialScore: 88, - practicalScore: 81, - riskScore: 18, - placementNotes: - 'Apartment Fit: 82%\n\nGood fit with existing residents. Slightly more extroverted but compatible.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: sara.id, - housingUnitId: unit5.id, - spotId: unit5Bed4.id, - startDate: sixMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 79, - lifestyleScore: 82, - socialScore: 85, - practicalScore: 75, - riskScore: 21, - placementNotes: - 'Apartment Fit: 79%\n\nHighly clean, might set good example. Needs quiet which aligns with unit culture.', - }, + await dbClient.insert(placement).values({ + residentId: fatima.id, + housingUnitId: unit5.id, + spotId: unit5Bed1.id, + startDate: sixMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 88, + lifestyleScore: 85, + socialScore: 92, + practicalScore: 87, + riskScore: 12, + placementNotes: + 'Apartment Fit: 88%\n\nStrong match - similar cultural background and lifestyle preferences.', + }) + + await dbClient.insert(placement).values({ + residentId: yasmin.id, + housingUnitId: unit5.id, + spotId: unit5Bed2.id, + startDate: sixMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 85, + lifestyleScore: 83, + socialScore: 90, + practicalScore: 84, + riskScore: 15, + placementNotes: + 'Apartment Fit: 85%\n\nExcellent language match with Fatima (both Arabic speakers).', + }) + + await dbClient.insert(placement).values({ + residentId: amira.id, + housingUnitId: unit5.id, + spotId: unit5Bed3.id, + startDate: sixMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 82, + lifestyleScore: 80, + socialScore: 88, + practicalScore: 81, + riskScore: 18, + placementNotes: + 'Apartment Fit: 82%\n\nGood fit with existing residents. Slightly more extroverted but compatible.', + }) + + await dbClient.insert(placement).values({ + residentId: sara.id, + housingUnitId: unit5.id, + spotId: unit5Bed4.id, + startDate: sixMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 79, + lifestyleScore: 82, + socialScore: 85, + practicalScore: 75, + riskScore: 21, + placementNotes: + 'Apartment Fit: 79%\n\nHighly clean, might set good example. Needs quiet which aligns with unit culture.', }) // Unit 12 placements (PROBLEM - low compatibility, conflicts expected) - await prisma.placement.create({ - data: { - residentId: marco.id, - housingUnitId: unit12.id, - spotId: unit12Bed1.id, - startDate: threeMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 45, - lifestyleScore: 38, - socialScore: 52, - practicalScore: 48, - riskScore: 62, - placementNotes: 'Apartment Fit: 45%\n\nSuboptimal match - significant lifestyle differences.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: dmitri.id, - housingUnitId: unit12.id, - spotId: unit12Bed2.id, - startDate: threeMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 42, - lifestyleScore: 35, - socialScore: 48, - practicalScore: 45, - riskScore: 58, - placementNotes: - 'Apartment Fit: 42%\n\nLanguage barrier with Marco. Both low on chores contribution.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: petro.id, - housingUnitId: unit12.id, - spotId: unit12Bed3.id, - startDate: twoMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 48, - lifestyleScore: 40, - socialScore: 55, - practicalScore: 50, - riskScore: 52, - placementNotes: 'Apartment Fit: 48%\n\nBetter than existing residents but still suboptimal.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: alexei.id, - housingUnitId: unit12.id, - spotId: unit12Room3.id, - startDate: twoMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 28, - lifestyleScore: 22, - socialScore: 35, - practicalScore: 30, - riskScore: 78, - placementNotes: - 'Apartment Fit: 28%\n\nVery poor match - extremely clean person in messy unit. High conflict risk.', - }, + await dbClient.insert(placement).values({ + residentId: marco.id, + housingUnitId: unit12.id, + spotId: unit12Bed1.id, + startDate: threeMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 45, + lifestyleScore: 38, + socialScore: 52, + practicalScore: 48, + riskScore: 62, + placementNotes: 'Apartment Fit: 45%\n\nSuboptimal match - significant lifestyle differences.', + }) + + await dbClient.insert(placement).values({ + residentId: dmitri.id, + housingUnitId: unit12.id, + spotId: unit12Bed2.id, + startDate: threeMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 42, + lifestyleScore: 35, + socialScore: 48, + practicalScore: 45, + riskScore: 58, + placementNotes: + 'Apartment Fit: 42%\n\nLanguage barrier with Marco. Both low on chores contribution.', + }) + + await dbClient.insert(placement).values({ + residentId: petro.id, + housingUnitId: unit12.id, + spotId: unit12Bed3.id, + startDate: twoMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 48, + lifestyleScore: 40, + socialScore: 55, + practicalScore: 50, + riskScore: 52, + placementNotes: 'Apartment Fit: 48%\n\nBetter than existing residents but still suboptimal.', + }) + + await dbClient.insert(placement).values({ + residentId: alexei.id, + housingUnitId: unit12.id, + spotId: unit12Room3.id, + startDate: twoMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 28, + lifestyleScore: 22, + socialScore: 35, + practicalScore: 30, + riskScore: 78, + placementNotes: + 'Apartment Fit: 28%\n\nVery poor match - extremely clean person in messy unit. High conflict risk.', }) // Unit 7 placements (GOOD - ready for Ahmed) - await prisma.placement.create({ - data: { - residentId: habib.id, - housingUnitId: unit7.id, - spotId: unit7Bed1.id, - startDate: threeMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 82, - lifestyleScore: 80, - socialScore: 88, - practicalScore: 80, - riskScore: 18, - placementNotes: 'Apartment Fit: 82%\n\nGood foundational resident for unit.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: omar.id, - housingUnitId: unit7.id, - spotId: unit7Bed2.id, - startDate: threeMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 80, - lifestyleScore: 78, - socialScore: 86, - practicalScore: 78, - riskScore: 20, - placementNotes: 'Apartment Fit: 80%\n\nStrong language match with Habib.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: mustafa.id, - housingUnitId: unit7.id, - spotId: unit7Bed3.id, - startDate: twoMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 76, - lifestyleScore: 75, - socialScore: 80, - practicalScore: 74, - riskScore: 24, - placementNotes: - 'Apartment Fit: 76%\n\nGood addition. Similar early bird schedule with Habib.', - }, + await dbClient.insert(placement).values({ + residentId: habib.id, + housingUnitId: unit7.id, + spotId: unit7Bed1.id, + startDate: threeMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 82, + lifestyleScore: 80, + socialScore: 88, + practicalScore: 80, + riskScore: 18, + placementNotes: 'Apartment Fit: 82%\n\nGood foundational resident for unit.', + }) + + await dbClient.insert(placement).values({ + residentId: omar.id, + housingUnitId: unit7.id, + spotId: unit7Bed2.id, + startDate: threeMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 80, + lifestyleScore: 78, + socialScore: 86, + practicalScore: 78, + riskScore: 20, + placementNotes: 'Apartment Fit: 80%\n\nStrong language match with Habib.', + }) + + await dbClient.insert(placement).values({ + residentId: mustafa.id, + housingUnitId: unit7.id, + spotId: unit7Bed3.id, + startDate: twoMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 76, + lifestyleScore: 75, + socialScore: 80, + practicalScore: 74, + riskScore: 24, + placementNotes: 'Apartment Fit: 76%\n\nGood addition. Similar early bird schedule with Habib.', }) // Unit 9 placements - await prisma.placement.create({ - data: { - residentId: elena.id, - housingUnitId: unit9.id, - spotId: unit9Bed1.id, - startDate: threeMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 68, - lifestyleScore: 65, - socialScore: 72, - practicalScore: 67, - riskScore: 33, - placementNotes: 'Apartment Fit: 68%\n\nModerate match.', - }, - }) - - await prisma.placement.create({ - data: { - residentId: grace.id, - housingUnitId: unit9.id, - spotId: unit9Bed2.id, - startDate: twoMonthsAgo, - status: 'ACTIVE', - compatibilityScore: 72, - lifestyleScore: 70, - socialScore: 75, - practicalScore: 71, - riskScore: 28, - placementNotes: 'Apartment Fit: 72%\n\nGood addition to unit.', - }, + await dbClient.insert(placement).values({ + residentId: elena.id, + housingUnitId: unit9.id, + spotId: unit9Bed1.id, + startDate: threeMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 68, + lifestyleScore: 65, + socialScore: 72, + practicalScore: 67, + riskScore: 33, + placementNotes: 'Apartment Fit: 68%\n\nModerate match.', + }) + + await dbClient.insert(placement).values({ + residentId: grace.id, + housingUnitId: unit9.id, + spotId: unit9Bed2.id, + startDate: twoMonthsAgo, + status: 'ACTIVE', + compatibilityScore: 72, + lifestyleScore: 70, + socialScore: 75, + practicalScore: 71, + riskScore: 28, + placementNotes: 'Apartment Fit: 72%\n\nGood addition to unit.', }) // ============================================================================ @@ -1198,8 +1211,9 @@ export async function seedDemoData( // Unit 12 - PROBLEM UNIT: Multiple conflicts demonstrating poor compatibility // Week 2: First cleanliness complaint (predicted timing) - const incident1 = await prisma.incident.create({ - data: { + const [incident1] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'CLEANLINESS_DISPUTE', @@ -1210,20 +1224,19 @@ export async function seedDemoData( resolvedAt: new Date(Date.now() - 83 * 24 * 60 * 60 * 1000), resolution: 'Hausordnung besprochen. Putzplan erstellt.', reportedById: marco.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident1.id, residentId: marco.id, role: 'INVOLVED' }, - { incidentId: incident1.id, residentId: dmitri.id, role: 'INVOLVED' }, - { incidentId: incident1.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident1.id, residentId: marco.id, role: 'INVOLVED' }, + { incidentId: incident1.id, residentId: dmitri.id, role: 'INVOLVED' }, + { incidentId: incident1.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Week 3: Noise complaint (night owl vs introverted needs privacy) - const incident2 = await prisma.incident.create({ - data: { + const [incident2] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'NOISE_COMPLAINT', @@ -1234,19 +1247,18 @@ export async function seedDemoData( resolvedAt: new Date(Date.now() - 76 * 24 * 60 * 60 * 1000), resolution: 'Ruhezeiten nach 22:00 Uhr vereinbart. Petro zugestimmt.', reportedById: alexei.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident2.id, residentId: alexei.id, role: 'INVOLVED' }, - { incidentId: incident2.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident2.id, residentId: alexei.id, role: 'INVOLVED' }, + { incidentId: incident2.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Week 4: Cleanliness escalation (difference of 3 levels) - const incident3 = await prisma.incident.create({ - data: { + const [incident3] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'CLEANLINESS_DISPUTE', @@ -1255,20 +1267,19 @@ export async function seedDemoData( 'Schwerer Sauberkeitskonflikt: Alexei (sehr sauber) kann nicht mit Dmitri und Petro (beide Sauberkeit Level 2) zusammenleben. Küche nicht gereinigt seit 5 Tagen.', severity: 'HIGH', // No reportedById = staff reported - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident3.id, residentId: alexei.id, role: 'INVOLVED' }, - { incidentId: incident3.id, residentId: dmitri.id, role: 'INVOLVED' }, - { incidentId: incident3.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident3.id, residentId: alexei.id, role: 'INVOLVED' }, + { incidentId: incident3.id, residentId: dmitri.id, role: 'INVOLVED' }, + { incidentId: incident3.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Week 6: Chores conflict (low contribution causing tension) - const incident4 = await prisma.incident.create({ - data: { + const [incident4] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'PERSONAL_CONFLICT', @@ -1279,19 +1290,18 @@ export async function seedDemoData( resolvedAt: new Date(Date.now() - 52 * 24 * 60 * 60 * 1000), resolution: 'Rotierender Putzplan mit klaren Zuständigkeiten eingeführt.', reportedById: marco.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident4.id, residentId: marco.id, role: 'INVOLVED' }, - { incidentId: incident4.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident4.id, residentId: marco.id, role: 'INVOLVED' }, + { incidentId: incident4.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Week 9: Recycling dispute (knowledge gap causing issues) - const incident5 = await prisma.incident.create({ - data: { + const [incident5] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'PERSONAL_CONFLICT', @@ -1302,20 +1312,19 @@ export async function seedDemoData( resolvedAt: new Date(Date.now() - 33 * 24 * 60 * 60 * 1000), resolution: 'Recycling-Schulung durchgeführt. Infografik in Küche aufgehängt.', reportedById: alexei.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident5.id, residentId: alexei.id, role: 'INVOLVED' }, - { incidentId: incident5.id, residentId: dmitri.id, role: 'INVOLVED' }, - { incidentId: incident5.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident5.id, residentId: alexei.id, role: 'INVOLVED' }, + { incidentId: incident5.id, residentId: dmitri.id, role: 'INVOLVED' }, + { incidentId: incident5.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Week 11: Recent noise complaint (pattern continues) - const incident6 = await prisma.incident.create({ - data: { + const [incident6] = await dbClient + .insert(incident) + .values({ housingUnitId: unit12.id, category: 'INTERPERSONAL', type: 'NOISE_COMPLAINT', @@ -1324,22 +1333,21 @@ export async function seedDemoData( 'Erneute Lärmbelästigung: Petro hält sich nicht an vereinbarte Ruhezeiten. Alexei erwägt Umzug.', severity: 'HIGH', reportedById: alexei.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident6.id, residentId: alexei.id, role: 'INVOLVED' }, - { incidentId: incident6.id, residentId: petro.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident6.id, residentId: alexei.id, role: 'INVOLVED' }, + { incidentId: incident6.id, residentId: petro.id, role: 'INVOLVED' }, + ]) // Unit 5 - SUCCESS STORY: 0 incidents over 6 months (no incidents to create) // This demonstrates what good compatibility looks like // Unit 9 - One minor incident (manageable with moderate compatibility) - const incident7 = await prisma.incident.create({ - data: { + const [incident7] = await dbClient + .insert(incident) + .values({ housingUnitId: unit9.id, category: 'INTERPERSONAL', type: 'PERSONAL_CONFLICT', @@ -1350,15 +1358,13 @@ export async function seedDemoData( resolvedAt: new Date(Date.now() - 44 * 24 * 60 * 60 * 1000), resolution: 'Nutzungsplan erstellt. Beide Parteien zufrieden.', reportedById: elena.id, - }, - }) + }) + .returning() - await prisma.incidentInvolvement.createMany({ - data: [ - { incidentId: incident7.id, residentId: elena.id, role: 'INVOLVED' }, - { incidentId: incident7.id, residentId: grace.id, role: 'INVOLVED' }, - ], - }) + await dbClient.insert(incidentInvolvement).values([ + { incidentId: incident7.id, residentId: elena.id, role: 'INVOLVED' }, + { incidentId: incident7.id, residentId: grace.id, role: 'INVOLVED' }, + ]) // ======================================================================== // SHARED EXPENSES (Unit 5) — the expense-sharing tour @@ -1366,48 +1372,65 @@ export async function seedDemoData( // An equal 4-way split of CHF 48.00 with one settlement already recorded, // so the demo shows balances, a suggested transfer AND a payment history. const unit5MemberIds = [fatima.id, yasmin.id, amira.id, sara.id] - const groceries = await prisma.expense.create({ - data: { - housingUnitId: unit5.id, - paidById: fatima.id, - createdById: fatima.id, - description: 'Wocheneinkauf Migros', - category: 'GROCERIES', - amountRappen: 4800, - date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), - shares: { - create: unit5MemberIds.map((residentId) => ({ residentId, amountRappen: 1200 })), - }, - }, - }) - await prisma.expense.create({ - data: { - housingUnitId: unit5.id, - paidById: yasmin.id, - createdById: yasmin.id, - description: 'Putzmittel und WC-Papier', - category: 'HOUSEHOLD', - amountRappen: 1860, - date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), - shares: { - create: unit5MemberIds.map((residentId) => ({ residentId, amountRappen: 465 })), - }, - }, - }) - await prisma.settlement.create({ - data: { - housingUnitId: unit5.id, - fromId: sara.id, - toId: fatima.id, - amountRappen: 1200, - note: `Anteil ${groceries.description}`, - }, + const groceries = await dbClient.transaction(async (tx) => { + const [created] = await tx + .insert(expense) + .values({ + housingUnitId: unit5.id, + paidById: fatima.id, + createdById: fatima.id, + description: 'Wocheneinkauf Migros', + category: 'GROCERIES', + amountRappen: 4800, + date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), + }) + .returning() + await tx + .insert(expenseShare) + .values( + unit5MemberIds.map((residentId) => ({ + expenseId: created.id, + residentId, + amountRappen: 1200, + })), + ) + return created + }) + await dbClient.transaction(async (tx) => { + const [created] = await tx + .insert(expense) + .values({ + housingUnitId: unit5.id, + paidById: yasmin.id, + createdById: yasmin.id, + description: 'Putzmittel und WC-Papier', + category: 'HOUSEHOLD', + amountRappen: 1860, + date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), + }) + .returning() + await tx + .insert(expenseShare) + .values( + unit5MemberIds.map((residentId) => ({ + expenseId: created.id, + residentId, + amountRappen: 465, + })), + ) + }) + await dbClient.insert(settlement).values({ + housingUnitId: unit5.id, + fromId: sara.id, + toId: fatima.id, + amountRappen: 1200, + note: `Anteil ${groceries.description}`, }) // ======================================================================== // LIVING TOGETHER (Unit 5) — chores, decisions and maintenance // ======================================================================== - await seedDemoGovernance(prisma, { + await seedDemoGovernance(dbClient, { siteWideContent: options.siteWideContent ?? false, unitId: unit5.id, demoResidentId: fatima.id, @@ -1421,13 +1444,14 @@ export async function seedDemoData( // and a board holding four people out of fifteen looks like a filter bug. // Ids are queried by prefix rather than threaded through this function, so // adding a resident above needs no change here. - const demoResidents = await prisma.resident.findMany({ - where: { - OR: [{ code: { startsWith: DEMO_RESIDENT_CODE_PREFIX } }, { code: demoResidentCode }], - }, - select: { id: true }, - }) - const integration = await seedIntegrationEvidence(prisma, { + const demoResidents = await dbClient.query.resident.findMany({ + where: or( + like(resident.code, `${escapeLike(DEMO_RESIDENT_CODE_PREFIX)}%`), + eq(resident.code, demoResidentCode), + ), + columns: { id: true }, + }) + const integration = await seedIntegrationEvidence(dbClient, { residentIds: demoResidents.map((resident) => resident.id), staffId: options.careStaffId ?? null, }) @@ -1435,16 +1459,22 @@ export async function seedDemoData( // Counts are queried, not hardcoded, so the summary can never drift from // the data above (ground truth #2: one source of truth). Scoped to the // demo prefixes: under UNIT scope this database also holds real data. - const demoUnitFilter = { code: { startsWith: DEMO_UNIT_CODE_PREFIX } } + const demoUnitFilter = like(housingUnit.code, `${escapeLike(DEMO_UNIT_CODE_PREFIX)}%`) + const demoUnitIds = dbClient + .select({ id: housingUnit.id }) + .from(housingUnit) + .where(demoUnitFilter) const [residents, housingUnits, placements, incidents] = await Promise.all([ - prisma.resident.count({ - where: { - OR: [{ code: { startsWith: DEMO_RESIDENT_CODE_PREFIX } }, { code: demoResidentCode }], - }, - }), - prisma.housingUnit.count({ where: demoUnitFilter }), - prisma.placement.count({ where: { housingUnit: demoUnitFilter } }), - prisma.incident.count({ where: { housingUnit: demoUnitFilter } }), + dbClient.$count( + resident, + or( + like(resident.code, `${escapeLike(DEMO_RESIDENT_CODE_PREFIX)}%`), + eq(resident.code, demoResidentCode), + ), + ), + dbClient.$count(housingUnit, demoUnitFilter), + dbClient.$count(placement, inArray(placement.housingUnitId, demoUnitIds)), + dbClient.$count(incident, inArray(incident.housingUnitId, demoUnitIds)), ]) return { diff --git a/src/lib/demo/seed-governance.ts b/src/lib/demo/seed-governance.ts index 4d0768bc..9a4dff2d 100644 --- a/src/lib/demo/seed-governance.ts +++ b/src/lib/demo/seed-governance.ts @@ -15,7 +15,21 @@ * Relative-import-safe (no '@/' aliases): loaded through ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' +import { + activity, + eventRsvp, + houseEvent, + houseRule, + householdTask, + maintenanceRequest, + marketplacePost, + proposal, + taskAttentionFlag, + taskCompletion, + vote, + type db, +} from '../db' import { CATEGORY_DECISION_MODE, CATEGORY_THRESHOLD, @@ -77,7 +91,7 @@ function snapshotFor(category: 'NOISE' | 'KITCHEN' | 'CLEANLINESS' | 'SAFETY', v } export async function seedDemoGovernance( - prisma: PrismaClient, + dbClient: typeof db, ctx: DemoGovernanceContext, ): Promise { const { unitId, demoResidentId, roommateIds, siteWideContent } = ctx @@ -89,8 +103,9 @@ export async function seedDemoGovernance( // ========================================================================== // Completion counts are uneven on purpose: an even board shows nothing, while // "who has actually been doing this" is the conversation the page exists for. - const kitchen = await prisma.householdTask.create({ - data: { + const [kitchen] = await dbClient + .insert(householdTask) + .values({ housingUnitId: unitId, createdByResidentId: demoResidentId, title: 'Küche putzen', @@ -109,11 +124,12 @@ export async function seedDemoGovernance( ], // A rota so the demo shows whose turn it is; the order is just the house. rotationResidentIds: [demoResidentId, yasmin, amira, sara], - }, - }) + }) + .returning() - const trash = await prisma.householdTask.create({ - data: { + const [trash] = await dbClient + .insert(householdTask) + .values({ housingUnitId: unitId, createdByResidentId: yasmin, title: 'Abfall und Recycling rausbringen', @@ -126,11 +142,12 @@ export async function seedDemoGovernance( // uniform list where nothing ever needs doing. currentStatus: 'NEEDS_ATTENTION', checklist: ['Sack zum Container gebracht', 'Neuer Sack eingesetzt'], - }, - }) + }) + .returning() - const bathroom = await prisma.householdTask.create({ - data: { + const [bathroom] = await dbClient + .insert(householdTask) + .values({ housingUnitId: unitId, createdByResidentId: amira, title: 'Bad putzen', @@ -149,74 +166,70 @@ export async function seedDemoGovernance( 'Handtücher gewechselt', ], rotationResidentIds: [amira, demoResidentId, yasmin], - }, - }) + }) + .returning() // Minutes, not row counts, are what the balance panel reads — so the seeded // record is uneven in TIME as well as in count. Sara has done nothing this // month and sits visibly behind; that gap is the conversation the panel is // for, and an even board would show a visitor nothing at all. - const kitchenItems = kitchen.checklist - const bathItems = bathroom.checklist - const trashItems = trash.checklist - - await prisma.taskCompletion.createMany({ - data: [ - { - taskId: kitchen.id, - completedById: demoResidentId, - completedAt: daysAgoThisMonth(2), - durationMinutes: 35, - completedItems: kitchenItems, - }, - { - taskId: kitchen.id, - completedById: yasmin, - completedAt: daysAgoThisMonth(9), - durationMinutes: 30, - completedItems: kitchenItems, - }, - { - taskId: bathroom.id, - completedById: demoResidentId, - completedAt: daysAgoThisMonth(4), - durationMinutes: 20, - completedItems: bathItems, - }, - // A deliberately PARTIAL completion: the floor and the towels were left. - // "Erledigt" with two items unticked is exactly the situation the - // checklist exists to make visible instead of arguable. - { - taskId: bathroom.id, - completedById: amira, - completedAt: daysAgoThisMonth(11), - durationMinutes: 25, - completedItems: bathItems.slice(0, 3), - }, - { - taskId: trash.id, - completedById: yasmin, - completedAt: daysAgoThisMonth(3), - durationMinutes: 10, - completedItems: trashItems, - }, - { - taskId: trash.id, - completedById: demoResidentId, - completedAt: daysAgoThisMonth(10), - durationMinutes: 10, - completedItems: trashItems, - }, - ], - }) - - await prisma.taskAttentionFlag.create({ - data: { + const kitchenItems = kitchen.checklist ?? [] + const bathItems = bathroom.checklist ?? [] + const trashItems = trash.checklist ?? [] + + await dbClient.insert(taskCompletion).values([ + { + taskId: kitchen.id, + completedById: demoResidentId, + completedAt: daysAgoThisMonth(2), + durationMinutes: 35, + completedItems: kitchenItems, + }, + { + taskId: kitchen.id, + completedById: yasmin, + completedAt: daysAgoThisMonth(9), + durationMinutes: 30, + completedItems: kitchenItems, + }, + { + taskId: bathroom.id, + completedById: demoResidentId, + completedAt: daysAgoThisMonth(4), + durationMinutes: 20, + completedItems: bathItems, + }, + // A deliberately PARTIAL completion: the floor and the towels were left. + // "Erledigt" with two items unticked is exactly the situation the + // checklist exists to make visible instead of arguable. + { + taskId: bathroom.id, + completedById: amira, + completedAt: daysAgoThisMonth(11), + durationMinutes: 25, + completedItems: bathItems.slice(0, 3), + }, + { taskId: trash.id, - flaggedById: sara, - message: 'Der Abfallsack ist voll — kann das jemand heute noch rausbringen?', - createdAt: daysAgo(1), + completedById: yasmin, + completedAt: daysAgoThisMonth(3), + durationMinutes: 10, + completedItems: trashItems, }, + { + taskId: trash.id, + completedById: demoResidentId, + completedAt: daysAgoThisMonth(10), + durationMinutes: 10, + completedItems: trashItems, + }, + ]) + + await dbClient.insert(taskAttentionFlag).values({ + taskId: trash.id, + flaggedById: sara, + message: 'Der Abfallsack ist voll — kann das jemand heute noch rausbringen?', + createdAt: daysAgo(1), }) // ========================================================================== @@ -227,59 +240,59 @@ export async function seedDemoGovernance( // Backdated so the discussion window has genuinely elapsed; without that // the vote buttons refuse ("Die Abstimmung hat noch nicht begonnen") and // the single most important screen in the tour is unreachable. - await prisma.proposal.create({ - data: { - housingUnitId: unitId, - type: 'HOUSE_DECISION', - category: 'KITCHEN', - title: 'Abwasch am selben Abend', - body: - 'Wer kocht, wäscht am selben Abend ab. Das Geschirr bleibt nicht über Nacht stehen, ' + - 'damit die Küche am Morgen für alle nutzbar ist.', - proposedByResidentId: yasmin, - status: 'VOTING', - ...snapshotFor('KITCHEN', voters), - createdAt: daysAgo(5), - discussionEndsAt: daysAgo(2), - votingOpenedAt: daysAgo(2), - votingEndsAt: daysAhead(5), - votes: { - create: [ - { residentId: yasmin, choice: 'YES', castAt: daysAgo(2) }, - { residentId: amira, choice: 'YES', castAt: daysAgo(1) }, - // Sara is against; Fatima — the demo login — has not voted yet. - { - residentId: sara, - choice: 'NO', - reason: 'Nach der Spätschicht schaffe ich das nicht immer.', - castAt: daysAgo(1), - }, - ], + await dbClient.transaction(async (tx) => { + const [dishesProposal] = await tx + .insert(proposal) + .values({ + housingUnitId: unitId, + type: 'HOUSE_DECISION', + category: 'KITCHEN', + title: 'Abwasch am selben Abend', + body: + 'Wer kocht, wäscht am selben Abend ab. Das Geschirr bleibt nicht über Nacht stehen, ' + + 'damit die Küche am Morgen für alle nutzbar ist.', + proposedByResidentId: yasmin, + status: 'VOTING', + ...snapshotFor('KITCHEN', voters), + createdAt: daysAgo(5), + discussionEndsAt: daysAgo(2), + votingOpenedAt: daysAgo(2), + votingEndsAt: daysAhead(5), + }) + .returning() + await tx.insert(vote).values([ + { proposalId: dishesProposal.id, residentId: yasmin, choice: 'YES', castAt: daysAgo(2) }, + { proposalId: dishesProposal.id, residentId: amira, choice: 'YES', castAt: daysAgo(1) }, + // Sara is against; Fatima — the demo login — has not voted yet. + { + proposalId: dishesProposal.id, + residentId: sara, + choice: 'NO', + reason: 'Nach der Spätschicht schaffe ich das nicht immer.', + castAt: daysAgo(1), }, - }, + ]) }) // 2. DISCUSSION — still being talked about, voting has not opened. - await prisma.proposal.create({ - data: { - housingUnitId: unitId, - type: 'HOUSE_DECISION', - category: 'SHARED_SPACES', - title: 'Pflanzen im Wohnzimmer', - body: - 'Wir stellen ein paar Pflanzen ins Wohnzimmer und teilen das Giessen auf. ' + - 'Kosten ca. CHF 40, aus der gemeinsamen Kasse.', - proposedByResidentId: amira, - status: 'DISCUSSION', - decisionMode: CATEGORY_DECISION_MODE.SHARED_SPACES, - threshold: CATEGORY_THRESHOLD.SHARED_SPACES, - quorumPercent: DECISION_TIMING.quorumPercent, - approvalPercent: THRESHOLD_APPROVAL_PERCENT[CATEGORY_THRESHOLD.SHARED_SPACES], - eligibleVoterCount: voters, - createdAt: daysAgo(1), - discussionEndsAt: daysAhead(2), - votingEndsAt: daysAhead(9), - }, + await dbClient.insert(proposal).values({ + housingUnitId: unitId, + type: 'HOUSE_DECISION', + category: 'SHARED_SPACES', + title: 'Pflanzen im Wohnzimmer', + body: + 'Wir stellen ein paar Pflanzen ins Wohnzimmer und teilen das Giessen auf. ' + + 'Kosten ca. CHF 40, aus der gemeinsamen Kasse.', + proposedByResidentId: amira, + status: 'DISCUSSION', + decisionMode: CATEGORY_DECISION_MODE.SHARED_SPACES, + threshold: CATEGORY_THRESHOLD.SHARED_SPACES, + quorumPercent: DECISION_TIMING.quorumPercent, + approvalPercent: THRESHOLD_APPROVAL_PERCENT[CATEGORY_THRESHOLD.SHARED_SPACES], + eligibleVoterCount: voters, + createdAt: daysAgo(1), + discussionEndsAt: daysAhead(2), + votingEndsAt: daysAhead(9), }) // 3. ACCEPTED — a decision the house already took, with the house rule it @@ -301,141 +314,137 @@ export async function seedDemoGovernance( approvalPercent: quietSnapshot.approvalPercent, }) - const nightQuiet = await prisma.houseRule.findUnique({ where: { key: 'night_quiet' } }) - - const quietProposal = await prisma.proposal.create({ - data: { - housingUnitId: unitId, - type: 'ADD_RULE', - category: 'NOISE', - title: 'Ruhe ab 21:30 statt 22:00', - body: - 'Wir ziehen die Nachtruhe eine halbe Stunde vor. Ab 21:30 keine Musik ohne Kopfhörer ' + - 'und keine Waschmaschine mehr.', - parentOrgRuleId: nightQuiet?.id ?? null, - proposedByResidentId: demoResidentId, - status: 'ACCEPTED', - ...quietSnapshot, - createdAt: daysAgo(24), - discussionEndsAt: daysAgo(21), - votingOpenedAt: daysAgo(21), - votingEndsAt: daysAgo(14), - decidedAt: daysAgo(14), - outcomeSummary: quietTally.explanation, - votes: { create: quietVotes.map((v) => ({ ...v, castAt: daysAgo(18) })) }, - }, + const nightQuiet = await dbClient.query.houseRule.findFirst({ + where: eq(houseRule.key, 'night_quiet'), }) - if (nightQuiet) { - await prisma.houseRule.create({ - data: { - scope: 'UNIT', + const quietProposal = await dbClient.transaction(async (tx) => { + const [created] = await tx + .insert(proposal) + .values({ housingUnitId: unitId, - parentRuleId: nightQuiet.id, + type: 'ADD_RULE', category: 'NOISE', - title: 'Ruhe ab 21:30', + title: 'Ruhe ab 21:30 statt 22:00', body: - 'In dieser Wohnung beginnt die Ruhezeit um 21:30 Uhr — eine halbe Stunde früher als ' + - `die ${BRAND.orgName}-Regel verlangt. Beschlossen von den Bewohnenden am ` + - `${daysAgo(14).toLocaleDateString('de-CH')}.`, - status: 'ACTIVE', - version: 1, - adoptedByProposalId: quietProposal.id, - }, + 'Wir ziehen die Nachtruhe eine halbe Stunde vor. Ab 21:30 keine Musik ohne Kopfhörer ' + + 'und keine Waschmaschine mehr.', + parentOrgRuleId: nightQuiet?.id ?? null, + proposedByResidentId: demoResidentId, + status: 'ACCEPTED', + ...quietSnapshot, + createdAt: daysAgo(24), + discussionEndsAt: daysAgo(21), + votingOpenedAt: daysAgo(21), + votingEndsAt: daysAgo(14), + decidedAt: daysAgo(14), + outcomeSummary: quietTally.explanation, + }) + .returning() + await tx + .insert(vote) + .values(quietVotes.map((v) => ({ ...v, proposalId: created.id, castAt: daysAgo(18) }))) + return created + }) + + if (nightQuiet) { + await dbClient.insert(houseRule).values({ + scope: 'UNIT', + housingUnitId: unitId, + parentRuleId: nightQuiet.id, + category: 'NOISE', + title: 'Ruhe ab 21:30', + body: + 'In dieser Wohnung beginnt die Ruhezeit um 21:30 Uhr — eine halbe Stunde früher als ' + + `die ${BRAND.orgName}-Regel verlangt. Beschlossen von den Bewohnenden am ` + + `${daysAgo(14).toLocaleDateString('de-CH')}.`, + status: 'ACTIVE', + version: 1, + adoptedByProposalId: quietProposal.id, }) } // 4. NEEDS_STAFF_CONFIRMATION — safety is never put to a vote, but the house // must still be answered. This is also what fills the staff decision queue, // which is otherwise empty in the staff tour. - await prisma.proposal.create({ - data: { - housingUnitId: unitId, - type: 'HOUSE_DECISION', - category: 'SAFETY', - title: 'Zweiter Schlüssel für den Veloraum', - body: - 'Es gibt nur einen Schlüssel für den Veloraum. Wir hätten gern einen zweiten, ' + - 'damit nicht immer dieselbe Person aufschliessen muss.', - proposedByResidentId: sara, - status: 'NEEDS_STAFF_CONFIRMATION', - ...snapshotFor('SAFETY', voters), - createdAt: daysAgo(3), - outcomeSummary: - 'Sicherheitsthemen werden nicht abgestimmt. Die Betreuung beantwortet den Vorschlag.', - }, + await dbClient.insert(proposal).values({ + housingUnitId: unitId, + type: 'HOUSE_DECISION', + category: 'SAFETY', + title: 'Zweiter Schlüssel für den Veloraum', + body: + 'Es gibt nur einen Schlüssel für den Veloraum. Wir hätten gern einen zweiten, ' + + 'damit nicht immer dieselbe Person aufschliessen muss.', + proposedByResidentId: sara, + status: 'NEEDS_STAFF_CONFIRMATION', + ...snapshotFor('SAFETY', voters), + createdAt: daysAgo(3), + outcomeSummary: + 'Sicherheitsthemen werden nicht abgestimmt. Die Betreuung beantwortet den Vorschlag.', }) // ========================================================================== // MAINTENANCE — the board staff work, seen from the resident side too // ========================================================================== - await prisma.maintenanceRequest.create({ - data: { - housingUnitId: unitId, - reportedById: demoResidentId, - category: 'PLUMBING', - priority: 'NORMAL', - title: 'Sanitär', - description: 'Der Wasserhahn im Bad tropft, auch wenn er ganz zugedreht ist.', - location: 'Bad', - status: 'IN_PROGRESS', - assignedTo: 'Hauswart', - assignedAt: daysAgo(2), - startedAt: daysAgo(1), - createdAt: daysAgo(3), - }, + await dbClient.insert(maintenanceRequest).values({ + housingUnitId: unitId, + reportedById: demoResidentId, + category: 'PLUMBING', + priority: 'NORMAL', + title: 'Sanitär', + description: 'Der Wasserhahn im Bad tropft, auch wenn er ganz zugedreht ist.', + location: 'Bad', + status: 'IN_PROGRESS', + assignedTo: 'Hauswart', + assignedAt: daysAgo(2), + startedAt: daysAgo(1), + createdAt: daysAgo(3), }) // Reported by the DEMO LOGIN and already answered, so the tour shows the one // thing a resident actually wants from reporting something: a reply. An // answered request belonging to a roommate proves nothing to the visitor. - await prisma.maintenanceRequest.create({ - data: { - housingUnitId: unitId, - reportedById: demoResidentId, - category: 'HEATING_COOLING', - priority: 'NORMAL', - title: 'Heizung/Klima', - description: 'Die Heizung im Zimmer wird nur oben warm.', - location: 'Zimmer', - status: 'COMPLETED', - assignedTo: 'Hauswart', - completedAt: daysAgo(6), - resolution: 'Heizkörper entlüftet. Bitte melden, falls es wieder auftritt.', - createdAt: daysAgo(12), - }, + await dbClient.insert(maintenanceRequest).values({ + housingUnitId: unitId, + reportedById: demoResidentId, + category: 'HEATING_COOLING', + priority: 'NORMAL', + title: 'Heizung/Klima', + description: 'Die Heizung im Zimmer wird nur oben warm.', + location: 'Zimmer', + status: 'COMPLETED', + assignedTo: 'Hauswart', + completedAt: daysAgo(6), + resolution: 'Heizkörper entlüftet. Bitte melden, falls es wieder auftritt.', + createdAt: daysAgo(12), }) - await prisma.maintenanceRequest.create({ - data: { - housingUnitId: unitId, - reportedById: sara, - category: 'APPLIANCE', - priority: 'HIGH', - title: 'Gerät defekt', - description: 'Die Waschmaschine schleudert nicht mehr und bleibt mitten im Programm stehen.', - location: 'Waschküche', - status: 'OPEN', - createdAt: daysAgo(1), - }, + await dbClient.insert(maintenanceRequest).values({ + housingUnitId: unitId, + reportedById: sara, + category: 'APPLIANCE', + priority: 'HIGH', + title: 'Gerät defekt', + description: 'Die Waschmaschine schleudert nicht mehr und bleibt mitten im Programm stehen.', + location: 'Waschküche', + status: 'OPEN', + createdAt: daysAgo(1), }) - await prisma.maintenanceRequest.create({ - data: { - housingUnitId: unitId, - reportedById: yasmin, - category: 'ELECTRICAL', - priority: 'NORMAL', - title: 'Elektrik', - description: 'Das Licht im Korridor flackert.', - location: 'Korridor', - status: 'COMPLETED', - assignedTo: 'Hauswart', - completedAt: daysAgo(5), - // The answer travels back to the resident who reported it. - resolution: 'Leuchtmittel und Starter ersetzt. Bitte melden, falls es erneut flackert.', - createdAt: daysAgo(9), - }, + await dbClient.insert(maintenanceRequest).values({ + housingUnitId: unitId, + reportedById: yasmin, + category: 'ELECTRICAL', + priority: 'NORMAL', + title: 'Elektrik', + description: 'Das Licht im Korridor flackert.', + location: 'Korridor', + status: 'COMPLETED', + assignedTo: 'Hauswart', + completedAt: daysAgo(5), + // The answer travels back to the resident who reported it. + resolution: 'Leuchtmittel und Starter ersetzt. Bitte melden, falls es erneut flackert.', + createdAt: daysAgo(9), }) // ========================================================================== @@ -448,68 +457,66 @@ export async function seedDemoGovernance( // // The states are deliberately mixed — open, claimed, closed — because a board // where nothing has ever been taken does not demonstrate a handover. - await prisma.marketplacePost.createMany({ - data: [ - { - housingUnitId: unitId, - postedById: yasmin, - title: 'Wasserkocher', - description: 'Funktioniert einwandfrei, ich habe jetzt zwei. Wer mag?', - kind: 'GIVE_AWAY', - category: 'KITCHEN', - status: 'OPEN', - contactNote: 'Zimmer 2, meistens ab 18 Uhr da.', - createdAt: daysAgo(2), - }, - { - housingUnitId: unitId, - postedById: amira, - title: 'Koffer für eine Woche', - description: 'Ich brauche einen grossen Koffer für eine Reise Ende Monat.', - kind: 'WANTED', - category: 'OTHER', - status: 'OPEN', - createdAt: daysAgo(4), - }, - { - housingUnitId: unitId, - postedById: sara, - title: 'Briefe auf Deutsch erklären', - description: - 'Ich kann Deutsch und Arabisch. Wenn ein Brief von einer Behörde kommt, lese ich ihn mit dir zusammen durch.', - kind: 'OFFER_HELP', - category: 'PAPERWORK', - status: 'OPEN', - contactNote: 'Klopf einfach, Zimmer 4.', - createdAt: daysAgo(6), - }, - { - housingUnitId: unitId, - postedById: demoResidentId, - title: 'Schrank in den 3. Stock tragen', - description: 'Zu zweit ist es in zehn Minuten erledigt. Samstagvormittag würde mir passen.', - kind: 'NEED_HELP', - category: 'MOVING', - status: 'CLAIMED', - claimedById: yasmin, - claimedAt: daysAgo(1), - createdAt: daysAgo(3), - }, - { - housingUnitId: unitId, - postedById: yasmin, - title: 'Kinderkleider Grösse 98', - description: 'Zwei Jacken und vier Pullover, alles gewaschen.', - kind: 'GIVE_AWAY', - category: 'KIDS', - status: 'CLOSED', - claimedById: amira, - claimedAt: daysAgo(9), - closedAt: daysAgo(8), - createdAt: daysAgo(11), - }, - ], - }) + await dbClient.insert(marketplacePost).values([ + { + housingUnitId: unitId, + postedById: yasmin, + title: 'Wasserkocher', + description: 'Funktioniert einwandfrei, ich habe jetzt zwei. Wer mag?', + kind: 'GIVE_AWAY', + category: 'KITCHEN', + status: 'OPEN', + contactNote: 'Zimmer 2, meistens ab 18 Uhr da.', + createdAt: daysAgo(2), + }, + { + housingUnitId: unitId, + postedById: amira, + title: 'Koffer für eine Woche', + description: 'Ich brauche einen grossen Koffer für eine Reise Ende Monat.', + kind: 'WANTED', + category: 'OTHER', + status: 'OPEN', + createdAt: daysAgo(4), + }, + { + housingUnitId: unitId, + postedById: sara, + title: 'Briefe auf Deutsch erklären', + description: + 'Ich kann Deutsch und Arabisch. Wenn ein Brief von einer Behörde kommt, lese ich ihn mit dir zusammen durch.', + kind: 'OFFER_HELP', + category: 'PAPERWORK', + status: 'OPEN', + contactNote: 'Klopf einfach, Zimmer 4.', + createdAt: daysAgo(6), + }, + { + housingUnitId: unitId, + postedById: demoResidentId, + title: 'Schrank in den 3. Stock tragen', + description: 'Zu zweit ist es in zehn Minuten erledigt. Samstagvormittag würde mir passen.', + kind: 'NEED_HELP', + category: 'MOVING', + status: 'CLAIMED', + claimedById: yasmin, + claimedAt: daysAgo(1), + createdAt: daysAgo(3), + }, + { + housingUnitId: unitId, + postedById: yasmin, + title: 'Kinderkleider Grösse 98', + description: 'Zwei Jacken und vier Pullover, alles gewaschen.', + kind: 'GIVE_AWAY', + category: 'KIDS', + status: 'CLOSED', + claimedById: amira, + claimedAt: daysAgo(9), + closedAt: daysAgo(8), + createdAt: daysAgo(11), + }, + ]) // ========================================================================== // HOUSE EVENTS — one past, one imminent, one further out @@ -517,8 +524,9 @@ export async function seedDemoGovernance( // The RSVP counts are uneven and the demo resident has deliberately NOT // answered the next one, so the visitor has a decision to make rather than a // finished record to read. - const houseMeeting = await prisma.houseEvent.create({ - data: { + const [houseMeeting] = await dbClient + .insert(houseEvent) + .values({ housingUnitId: unitId, createdByResidentId: yasmin, title: 'Hausversammlung', @@ -529,19 +537,18 @@ export async function seedDemoGovernance( startsAt: daysAhead(3), status: 'PUBLISHED', createdAt: daysAgo(2), - }, - }) + }) + .returning() - await prisma.eventRsvp.createMany({ - data: [ - { eventId: houseMeeting.id, residentId: yasmin, status: 'GOING' }, - { eventId: houseMeeting.id, residentId: amira, status: 'GOING' }, - { eventId: houseMeeting.id, residentId: sara, status: 'MAYBE' }, - ], - }) + await dbClient.insert(eventRsvp).values([ + { eventId: houseMeeting.id, residentId: yasmin, status: 'GOING' }, + { eventId: houseMeeting.id, residentId: amira, status: 'GOING' }, + { eventId: houseMeeting.id, residentId: sara, status: 'MAYBE' }, + ]) - const cooking = await prisma.houseEvent.create({ - data: { + const [cooking] = await dbClient + .insert(houseEvent) + .values({ housingUnitId: unitId, createdByResidentId: amira, title: 'Zusammen kochen', @@ -551,18 +558,17 @@ export async function seedDemoGovernance( startsAt: daysAhead(10), status: 'PUBLISHED', createdAt: daysAgo(1), - }, - }) + }) + .returning() - await prisma.eventRsvp.createMany({ - data: [ - { eventId: cooking.id, residentId: amira, status: 'GOING' }, - { eventId: cooking.id, residentId: demoResidentId, status: 'GOING' }, - ], - }) + await dbClient.insert(eventRsvp).values([ + { eventId: cooking.id, residentId: amira, status: 'GOING' }, + { eventId: cooking.id, residentId: demoResidentId, status: 'GOING' }, + ]) - const pastEvent = await prisma.houseEvent.create({ - data: { + const [pastEvent] = await dbClient + .insert(houseEvent) + .values({ housingUnitId: unitId, createdByResidentId: sara, title: 'Frühlingsputz im Hof', @@ -572,17 +578,15 @@ export async function seedDemoGovernance( startsAt: daysAgo(14), status: 'PUBLISHED', createdAt: daysAgo(21), - }, - }) + }) + .returning() - await prisma.eventRsvp.createMany({ - data: [ - { eventId: pastEvent.id, residentId: sara, status: 'GOING' }, - { eventId: pastEvent.id, residentId: yasmin, status: 'GOING' }, - { eventId: pastEvent.id, residentId: demoResidentId, status: 'GOING' }, - { eventId: pastEvent.id, residentId: amira, status: 'DECLINED' }, - ], - }) + await dbClient.insert(eventRsvp).values([ + { eventId: pastEvent.id, residentId: sara, status: 'GOING' }, + { eventId: pastEvent.id, residentId: yasmin, status: 'GOING' }, + { eventId: pastEvent.id, residentId: demoResidentId, status: 'GOING' }, + { eventId: pastEvent.id, residentId: amira, status: 'DECLINED' }, + ]) // ========================================================================== // ACTIVITIES — the external catalogue @@ -598,74 +602,72 @@ export async function seedDemoGovernance( // real residents invented offers with invented phone numbers. if (!siteWideContent) return - await prisma.activity.createMany({ - data: [ - { - title: 'Offenes Fussballtraining', - description: - 'Jeden Mittwoch, alle Niveaus, keine Anmeldung nötig. Fussballschuhe können vor Ort geliehen werden.', - category: 'SPORT', - cost: 'FREE', - location: 'Sportanlage Heerenschürli, Zürich', - schedule: 'Mittwoch 18:00–20:00', - status: 'PUBLISHED', - highlight: true, - }, - { - title: 'Deutsch-Konversation im Quartiertreff', - description: - 'Zwanglos Deutsch sprechen mit Freiwilligen. Einsteigen ist jederzeit möglich, auch mit wenig Vorkenntnissen.', - category: 'LANGUAGE', - cost: 'FREE', - location: 'Quartiertreff Hirslanden', - schedule: 'Dienstag und Donnerstag, 14:00–16:00', - status: 'PUBLISHED', - highlight: true, - }, - { - title: 'Museum für Gestaltung — Eintritt mit KulturLegi', - description: - 'Mit der KulturLegi ist der Eintritt stark vergünstigt. Ausstellungen wechseln alle paar Monate.', - category: 'CULTURE', - cost: 'REDUCED', - costNote: 'CHF 6 statt CHF 12 mit KulturLegi', - location: 'Ausstellungsstrasse 60, Zürich', - website: 'https://museum-gestaltung.ch', - status: 'PUBLISHED', - }, - { - title: 'Nachbarschaftscafé', - description: - 'Kaffee, Kuchen und Leute aus dem Quartier. Ein guter Ort, um jemanden kennenzulernen.', - category: 'COMMUNITY', - cost: 'FREE', - location: 'Gemeinschaftszentrum Witikon', - schedule: 'Jeden Freitagnachmittag', - status: 'PUBLISHED', - }, - { - title: 'Spielgruppe für Kinder von 2 bis 5', - description: - 'Betreute Spielgruppe am Vormittag. Die Eltern können bleiben oder etwas erledigen.', - category: 'FAMILY', - cost: 'REDUCED', - costNote: 'Nach Einkommen abgestuft; frag bei der Betreuung nach', - location: 'Familienzentrum Zürich Ost', - schedule: 'Montag bis Donnerstag, 9:00–11:30', - phone: '044 000 00 00', - status: 'PUBLISHED', - }, - { - title: 'Rechtsberatung für Asylsuchende', - description: - 'Kostenlose und vertrauliche Beratung zu Verfahren und Fristen. Dolmetschen kann organisiert werden.', - category: 'SUPPORT', - cost: 'FREE', - location: 'Beratungsstelle Zürich', - schedule: 'Montag 13:00–17:00, ohne Voranmeldung', - phone: '044 000 00 01', - status: 'PUBLISHED', - }, - ], - }) + await dbClient.insert(activity).values([ + { + title: 'Offenes Fussballtraining', + description: + 'Jeden Mittwoch, alle Niveaus, keine Anmeldung nötig. Fussballschuhe können vor Ort geliehen werden.', + category: 'SPORT', + cost: 'FREE', + location: 'Sportanlage Heerenschürli, Zürich', + schedule: 'Mittwoch 18:00–20:00', + status: 'PUBLISHED', + highlight: true, + }, + { + title: 'Deutsch-Konversation im Quartiertreff', + description: + 'Zwanglos Deutsch sprechen mit Freiwilligen. Einsteigen ist jederzeit möglich, auch mit wenig Vorkenntnissen.', + category: 'LANGUAGE', + cost: 'FREE', + location: 'Quartiertreff Hirslanden', + schedule: 'Dienstag und Donnerstag, 14:00–16:00', + status: 'PUBLISHED', + highlight: true, + }, + { + title: 'Museum für Gestaltung — Eintritt mit KulturLegi', + description: + 'Mit der KulturLegi ist der Eintritt stark vergünstigt. Ausstellungen wechseln alle paar Monate.', + category: 'CULTURE', + cost: 'REDUCED', + costNote: 'CHF 6 statt CHF 12 mit KulturLegi', + location: 'Ausstellungsstrasse 60, Zürich', + website: 'https://museum-gestaltung.ch', + status: 'PUBLISHED', + }, + { + title: 'Nachbarschaftscafé', + description: + 'Kaffee, Kuchen und Leute aus dem Quartier. Ein guter Ort, um jemanden kennenzulernen.', + category: 'COMMUNITY', + cost: 'FREE', + location: 'Gemeinschaftszentrum Witikon', + schedule: 'Jeden Freitagnachmittag', + status: 'PUBLISHED', + }, + { + title: 'Spielgruppe für Kinder von 2 bis 5', + description: + 'Betreute Spielgruppe am Vormittag. Die Eltern können bleiben oder etwas erledigen.', + category: 'FAMILY', + cost: 'REDUCED', + costNote: 'Nach Einkommen abgestuft; frag bei der Betreuung nach', + location: 'Familienzentrum Zürich Ost', + schedule: 'Montag bis Donnerstag, 9:00–11:30', + phone: '044 000 00 00', + status: 'PUBLISHED', + }, + { + title: 'Rechtsberatung für Asylsuchende', + description: + 'Kostenlose und vertrauliche Beratung zu Verfahren und Fristen. Dolmetschen kann organisiert werden.', + category: 'SUPPORT', + cost: 'FREE', + location: 'Beratungsstelle Zürich', + schedule: 'Montag 13:00–17:00, ohne Voranmeldung', + phone: '044 000 00 01', + status: 'PUBLISHED', + }, + ]) } diff --git a/src/lib/demo/staff.ts b/src/lib/demo/staff.ts index 1aa1cdad..5d40141b 100644 --- a/src/lib/demo/staff.ts +++ b/src/lib/demo/staff.ts @@ -8,7 +8,8 @@ * Relative-import-safe (no '@/' aliases): loaded through ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' +import { account, user, type db } from '../db' import { getDemoStaffCode, DEMO_STAFF_NAME } from './config' import { demoStaffDoors, demoStaffReachFor } from './roles' @@ -31,7 +32,7 @@ export interface DemoStaffAccount { * unclaimed code, so a drive-by visitor could otherwise attach their own email * and password to a demo door and lock out everyone after them. */ -export async function upsertDemoStaffRoles(prisma: PrismaClient): Promise { +export async function upsertDemoStaffRoles(dbClient: typeof db): Promise { const accounts: DemoStaffAccount[] = [] for (const door of demoStaffDoors()) { @@ -39,15 +40,17 @@ export async function upsertDemoStaffRoles(prisma: PrismaClient): Promise { +export async function upsertDemoStaff(dbClient: typeof db): Promise { const demoStaffCode = getDemoStaffCode() if (!demoStaffCode) return null - const user = await prisma.user.upsert({ - where: { code: demoStaffCode }, - update: { name: DEMO_STAFF_NAME, active: true, ...demoStaffReachFor('ADMIN') }, - create: { + const [upserted] = await dbClient + .insert(user) + .values({ code: demoStaffCode, name: DEMO_STAFF_NAME, role: 'ADMIN', ...demoStaffReachFor('ADMIN'), - }, - select: { id: true }, - }) + }) + .onConflictDoUpdate({ + target: user.code, + set: { name: DEMO_STAFF_NAME, active: true, ...demoStaffReachFor('ADMIN') }, + }) + .returning({ id: user.id }) // Also drop any account claimed on the demo code: a drive-by visitor may // have registered their own email + password on it (registration is open on // any unclaimed code). Without this, that claim would outlive every reset // and lock the next visitor out of the demo door. - await prisma.account.deleteMany({ where: { userId: user.id } }) + await dbClient.delete(account).where(eq(account.userId, upserted.id)) - return { id: user.id, code: demoStaffCode } + return { id: upserted.id, code: demoStaffCode } } diff --git a/src/lib/demo/wipe.ts b/src/lib/demo/wipe.ts index cb395d9c..5960dd8f 100644 --- a/src/lib/demo/wipe.ts +++ b/src/lib/demo/wipe.ts @@ -9,7 +9,8 @@ * Relative-import-safe (no '@/' aliases): loaded through ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { sql } from 'drizzle-orm' +import type { db } from '../db' /** * Tables that survive a wipe: @@ -26,18 +27,19 @@ export const KEEP_TABLES = new Set([ ]) /** Truncate every public table except the keep-list. Returns the wiped count. */ -export async function wipeAllExceptKeepList(prisma: PrismaClient): Promise { - const tables = await prisma.$queryRaw>` +export async function wipeAllExceptKeepList(dbClient: typeof db): Promise { + const { rows } = await dbClient.execute(sql` SELECT tablename FROM pg_tables WHERE schemaname = 'public' - ` + `) + const tables = rows as unknown as Array<{ tablename: string }> const wipe = tables.map((t) => t.tablename).filter((t) => !KEEP_TABLES.has(t)) if (wipe.length > 0) { // Table names come from pg_tables, not user input; quoting preserves the // PascalCase names Prisma creates. CASCADE clears FK order concerns — // none of the kept tables reference a wiped one. - await prisma.$executeRawUnsafe( - `TRUNCATE TABLE ${wipe.map((t) => `"${t}"`).join(', ')} RESTART IDENTITY CASCADE`, + await dbClient.execute( + sql.raw(`TRUNCATE TABLE ${wipe.map((t) => `"${t}"`).join(', ')} RESTART IDENTITY CASCADE`), ) } diff --git a/src/lib/env.ts b/src/lib/env.ts index 51e43749..790f5d8a 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -15,7 +15,7 @@ import { z } from 'zod' const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), - // Database — required everywhere except tests (jest mocks prisma) + // Database — required everywhere except tests (jest mocks the db module) DATABASE_URL: z.string().url().optional(), // Auth secrets diff --git a/src/lib/expenses/data.ts b/src/lib/expenses/data.ts index 7692da6a..a65aab1f 100644 --- a/src/lib/expenses/data.ts +++ b/src/lib/expenses/data.ts @@ -7,7 +7,8 @@ * part of the history is simply a wrong number. */ -import { prisma } from '@/lib/db' +import { db, expense, settlement } from '@/lib/db' +import { desc, eq } from 'drizzle-orm' import { getActiveUnitMembers, type UnitMember } from '@/lib/portal-auth' import { computeBalances, simplifyDebts, type Transfer } from './balances' @@ -43,9 +44,9 @@ export interface UnitExpenseData { export async function getUnitExpenseData(housingUnitId: string): Promise { const [members, expenses, settlements] = await Promise.all([ getActiveUnitMembers(housingUnitId), - prisma.expense.findMany({ - where: { housingUnitId }, - select: { + db.query.expense.findMany({ + where: eq(expense.housingUnitId, housingUnitId), + columns: { id: true, description: true, category: true, @@ -53,13 +54,13 @@ export async function getUnitExpenseData(housingUnitId: string): Promise { - const proposal = await prisma.proposal.findUnique({ - where: { id: proposalId }, - include: { parentOrgRule: true, targetRule: true }, + const proposal = await db.query.proposal.findFirst({ + where: eq(proposalTable.id, proposalId), + with: { parentOrgRule: true, targetRule: true }, }) if (!proposal) return @@ -28,41 +29,39 @@ export async function adoptProposal(proposalId: string): Promise { if (proposal.type === 'HOUSE_DECISION') return if (proposal.type === 'REPEAL_RULE' && proposal.targetRuleId) { - await prisma.houseRule.update({ - where: { id: proposal.targetRuleId }, - data: { status: 'ARCHIVED', effectiveUntil: new Date() }, - }) + await db + .update(houseRule) + .set({ status: 'ARCHIVED', effectiveUntil: new Date() }) + .where(eq(houseRule.id, proposal.targetRuleId)) return } if (proposal.type === 'AMEND_RULE' && proposal.targetRule) { - await prisma.houseRule.update({ - where: { id: proposal.targetRule.id }, - data: { + await db + .update(houseRule) + .set({ title: proposal.title, body: proposal.body, // New wording must be re-acknowledged by everyone the rule binds. version: proposal.targetRule.version + 1, adoptedByProposalId: proposal.id, - }, - }) + }) + .where(eq(houseRule.id, proposal.targetRule.id)) return } if (proposal.type === 'ADD_RULE' && proposal.parentOrgRule) { - await prisma.houseRule.create({ - data: { - scope: 'UNIT', - housingUnitId: proposal.housingUnitId, - parentRuleId: proposal.parentOrgRule.id, - category: proposal.parentOrgRule.category, - title: proposal.title, - body: proposal.body, - delegation: proposal.parentOrgRule.delegation, - status: 'ACTIVE', - version: 1, - adoptedByProposalId: proposal.id, - }, + await db.insert(houseRule).values({ + scope: 'UNIT', + housingUnitId: proposal.housingUnitId, + parentRuleId: proposal.parentOrgRule.id, + category: proposal.parentOrgRule.category, + title: proposal.title, + body: proposal.body, + delegation: proposal.parentOrgRule.delegation, + status: 'ACTIVE', + version: 1, + adoptedByProposalId: proposal.id, }) } } @@ -72,9 +71,9 @@ export async function adoptProposal(proposalId: string): Promise { * Uses the policy snapshot stored on the proposal, never today's config. */ export async function closeProposal(proposalId: string): Promise { - const proposal = await prisma.proposal.findUnique({ - where: { id: proposalId }, - include: { votes: { select: { choice: true, reason: true } }, parentOrgRule: true }, + const proposal = await db.query.proposal.findFirst({ + where: eq(proposalTable.id, proposalId), + with: { votes: { columns: { choice: true, reason: true } }, parentOrgRule: true }, }) if (!proposal || proposal.status !== 'VOTING') return null @@ -93,11 +92,12 @@ export async function closeProposal(proposalId: string): Promise const status = resolveProposalStatus(tally.outcome, proposal.decisionMode, requiresConfirmation) // Guarded update: if another caller closed this proposal first, do nothing. - const updated = await prisma.proposal.updateMany({ - where: { id: proposalId, status: 'VOTING' }, - data: { status, decidedAt: new Date(), outcomeSummary: tally.explanation }, - }) - if (updated.count === 0) return null + const updated = await db + .update(proposalTable) + .set({ status, decidedAt: new Date(), outcomeSummary: tally.explanation }) + .where(and(eq(proposalTable.id, proposalId), eq(proposalTable.status, 'VOTING'))) + .returning({ id: proposalTable.id }) + if (updated.length === 0) return null if (status === 'ACCEPTED') { await adoptProposal(proposalId) @@ -123,26 +123,35 @@ export async function advanceDueProposals( housingUnitId?: string, ): Promise { const result: LifecycleResult = { opened: 0, closed: 0 } - const unitFilter = housingUnitId ? { housingUnitId } : {} + const unitFilter = housingUnitId ? [eq(proposalTable.housingUnitId, housingUnitId)] : [] try { - const dueToOpen = await prisma.proposal.findMany({ - where: { ...unitFilter, status: 'DISCUSSION', discussionEndsAt: { lte: now } }, - select: { id: true, housingUnitId: true }, + const dueToOpen = await db.query.proposal.findMany({ + where: and( + ...unitFilter, + eq(proposalTable.status, 'DISCUSSION'), + lte(proposalTable.discussionEndsAt, now), + ), + columns: { id: true, housingUnitId: true }, }) for (const proposal of dueToOpen) { const eligibleVoterCount = await countEligibleVoters(proposal.housingUnitId, now) - const opened = await prisma.proposal.updateMany({ - where: { id: proposal.id, status: 'DISCUSSION' }, - data: { status: 'VOTING', votingOpenedAt: now, eligibleVoterCount }, - }) - result.opened += opened.count + const opened = await db + .update(proposalTable) + .set({ status: 'VOTING', votingOpenedAt: now, eligibleVoterCount }) + .where(and(eq(proposalTable.id, proposal.id), eq(proposalTable.status, 'DISCUSSION'))) + .returning({ id: proposalTable.id }) + result.opened += opened.length } - const dueToClose = await prisma.proposal.findMany({ - where: { ...unitFilter, status: 'VOTING', votingEndsAt: { lte: now } }, - select: { id: true }, + const dueToClose = await db.query.proposal.findMany({ + where: and( + ...unitFilter, + eq(proposalTable.status, 'VOTING'), + lte(proposalTable.votingEndsAt, now), + ), + columns: { id: true }, }) for (const proposal of dueToClose) { @@ -159,14 +168,14 @@ export async function advanceDueProposals( /** Residents with an active placement — the electorate. Mirrors queries.ts. */ async function countEligibleVoters(housingUnitId: string, now: Date): Promise { - const placements = await prisma.placement.findMany({ - where: { - housingUnitId, - status: 'ACTIVE', - startDate: { lte: now }, - OR: [{ endDate: null }, { endDate: { gt: now } }], - }, - select: { residentId: true }, + const placements = await db.query.placement.findMany({ + where: and( + eq(placement.housingUnitId, housingUnitId), + eq(placement.status, 'ACTIVE'), + lte(placement.startDate, now), + or(isNull(placement.endDate), gt(placement.endDate, now)), + ), + columns: { residentId: true }, }) return new Set(placements.map((p) => p.residentId)).size } @@ -180,10 +189,16 @@ export async function expireStaleAgreements(now = new Date(), graceDays = 7): Pr const cutoff = new Date(now) cutoff.setDate(cutoff.getDate() - graceDays) - const result = await prisma.conflictAgreement.updateMany({ - where: { status: { in: ['PROPOSED', 'ACCEPTED'] }, reviewDate: { lt: cutoff } }, - data: { status: 'EXPIRED' }, - }) - - return result.count + const expired = await db + .update(conflictAgreement) + .set({ status: 'EXPIRED' }) + .where( + and( + inArray(conflictAgreement.status, ['PROPOSED', 'ACCEPTED']), + lt(conflictAgreement.reviewDate, cutoff), + ), + ) + .returning({ id: conflictAgreement.id }) + + return expired.length } diff --git a/src/lib/governance/queries.ts b/src/lib/governance/queries.ts index 98c77f37..c2915ead 100644 --- a/src/lib/governance/queries.ts +++ b/src/lib/governance/queries.ts @@ -7,8 +7,9 @@ * be disputable. */ -import { prisma } from '@/lib/db' -import type { ProposalStatus, VoteChoice, VoteThreshold } from '@prisma/client' +import { db, houseRule, placement, proposal, ruleAcknowledgement } from '@/lib/db' +import type { ProposalStatus, VoteChoice, VoteThreshold } from '@/lib/db' +import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from 'drizzle-orm' import { buildRuleBook, type RuleBook, type RuleLike } from './rules' import { outstandingForResident, unitCoverage, type OutstandingRule } from './acknowledgement' import { tallyVotes, type TallyResult } from './voting' @@ -30,18 +31,18 @@ const RULE_SELECT = { } as const export async function getOrgRules(): Promise { - return prisma.houseRule.findMany({ - where: { scope: 'ORG' }, - select: RULE_SELECT, - orderBy: [{ category: 'asc' }, { title: 'asc' }], + return db.query.houseRule.findMany({ + where: eq(houseRule.scope, 'ORG'), + columns: RULE_SELECT, + orderBy: [asc(houseRule.category), asc(houseRule.title)], }) } export async function getUnitRules(housingUnitId: string): Promise { - return prisma.houseRule.findMany({ - where: { scope: 'UNIT', housingUnitId }, - select: RULE_SELECT, - orderBy: [{ category: 'asc' }, { createdAt: 'asc' }], + return db.query.houseRule.findMany({ + where: and(eq(houseRule.scope, 'UNIT'), eq(houseRule.housingUnitId, housingUnitId)), + columns: RULE_SELECT, + orderBy: [asc(houseRule.category), asc(houseRule.createdAt)], }) } @@ -60,14 +61,14 @@ export async function getUnitResidentIds( housingUnitId: string, now = new Date(), ): Promise { - const placements = await prisma.placement.findMany({ - where: { - housingUnitId, - status: 'ACTIVE', - startDate: { lte: now }, - OR: [{ endDate: null }, { endDate: { gt: now } }], - }, - select: { residentId: true }, + const placements = await db.query.placement.findMany({ + where: and( + eq(placement.housingUnitId, housingUnitId), + eq(placement.status, 'ACTIVE'), + lte(placement.startDate, now), + or(isNull(placement.endDate), gt(placement.endDate, now)), + ), + columns: { residentId: true }, }) return Array.from(new Set(placements.map((p) => p.residentId))) } @@ -91,10 +92,18 @@ export async function getOutstandingRules( ...book.orphanedUnitRules, ] - const acknowledgements = await prisma.ruleAcknowledgement.findMany({ - where: { residentId, ruleId: { in: bindingRules.map((r) => r.id) } }, - select: { ruleId: true, residentId: true, ruleVersion: true }, - }) + // An empty rule id list means no acknowledgements can match — skip the query + // (an empty `inArray` is an error, not an empty result). + const ruleIds = bindingRules.map((r) => r.id) + const acknowledgements = ruleIds.length + ? await db.query.ruleAcknowledgement.findMany({ + where: and( + eq(ruleAcknowledgement.residentId, residentId), + inArray(ruleAcknowledgement.ruleId, ruleIds), + ), + columns: { ruleId: true, residentId: true, ruleVersion: true }, + }) + : [] return outstandingForResident(bindingRules, acknowledgements, residentId) } @@ -111,10 +120,19 @@ export async function getUnitAcknowledgementCoverage(housingUnitId: string) { ...book.orphanedUnitRules, ] - const acknowledgements = await prisma.ruleAcknowledgement.findMany({ - where: { ruleId: { in: rules.map((r) => r.id) }, residentId: { in: residentIds } }, - select: { ruleId: true, residentId: true, ruleVersion: true }, - }) + // Either list being empty means no acknowledgement can match — skip the + // query (an empty `inArray` is an error, not an empty result). + const ruleIds = rules.map((r) => r.id) + const acknowledgements = + ruleIds.length && residentIds.length + ? await db.query.ruleAcknowledgement.findMany({ + where: and( + inArray(ruleAcknowledgement.ruleId, ruleIds), + inArray(ruleAcknowledgement.residentId, residentIds), + ), + columns: { ruleId: true, residentId: true, ruleVersion: true }, + }) + : [] return unitCoverage(rules, acknowledgements, residentIds) } @@ -124,31 +142,43 @@ export async function getUnitAcknowledgementCoverage(housingUnitId: string) { // ============================================================================= const PROPOSAL_INCLUDE = { - votes: { select: { id: true, choice: true, reason: true, residentId: true, castAt: true } }, - proposedByResident: { select: { id: true, code: true } }, - parentOrgRule: { select: { id: true, title: true, delegation: true, category: true } }, - targetRule: { select: { id: true, title: true } }, - housingUnit: { select: { id: true, code: true, address: true } }, + votes: { columns: { id: true, choice: true, reason: true, residentId: true, castAt: true } }, + proposedByResident: { columns: { id: true, code: true } }, + parentOrgRule: { columns: { id: true, title: true, delegation: true, category: true } }, + targetRule: { columns: { id: true, title: true } }, + housingUnit: { columns: { id: true, code: true, address: true } }, } as const export async function getUnitProposals(housingUnitId: string, statuses?: ProposalStatus[]) { - return prisma.proposal.findMany({ - where: { housingUnitId, ...(statuses ? { status: { in: statuses } } : {}) }, - include: PROPOSAL_INCLUDE, - orderBy: { createdAt: 'desc' }, + return db.query.proposal.findMany({ + where: and( + eq(proposal.housingUnitId, housingUnitId), + statuses + ? // An empty status list means "match nothing", which `inArray` cannot express. + statuses.length + ? inArray(proposal.status, statuses) + : sql`false` + : undefined, + ), + with: PROPOSAL_INCLUDE, + orderBy: [desc(proposal.createdAt)], }) } export async function getProposal(proposalId: string) { - return prisma.proposal.findUnique({ where: { id: proposalId }, include: PROPOSAL_INCLUDE }) + const row = await db.query.proposal.findFirst({ + where: eq(proposal.id, proposalId), + with: PROPOSAL_INCLUDE, + }) + return row ?? null } /** Proposals waiting on a staff decision, across all units. */ export async function getProposalsAwaitingStaff() { - return prisma.proposal.findMany({ - where: { status: 'NEEDS_STAFF_CONFIRMATION' }, - include: PROPOSAL_INCLUDE, - orderBy: { decidedAt: 'asc' }, + return db.query.proposal.findMany({ + where: eq(proposal.status, 'NEEDS_STAFF_CONFIRMATION'), + with: PROPOSAL_INCLUDE, + orderBy: [asc(proposal.decidedAt)], }) } diff --git a/src/lib/governance/rules.ts b/src/lib/governance/rules.ts index 38b3b360..5223744f 100644 --- a/src/lib/governance/rules.ts +++ b/src/lib/governance/rules.ts @@ -15,7 +15,7 @@ import type { RuleDelegation, RuleScope, RuleStatus, -} from '@prisma/client' +} from '@/lib/db' import { DELEGATION_ALLOWS_UNIT_RULE, DELEGATION_REQUIRES_STAFF_CONFIRMATION, diff --git a/src/lib/governance/sync-org-rules.ts b/src/lib/governance/sync-org-rules.ts index 529f4e30..833f04d8 100644 --- a/src/lib/governance/sync-org-rules.ts +++ b/src/lib/governance/sync-org-rules.ts @@ -11,9 +11,10 @@ * bound to wording they never saw. */ -import type { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' // Relative rather than the usual '@/' alias: prisma/seed.ts imports this module // through ts-node, which does not resolve tsconfig path aliases. +import { houseRule, type db } from '../db' import { ORG_RULE_CATALOG } from '../config/house-rules' export interface SyncResult { @@ -24,24 +25,24 @@ export interface SyncResult { amendedKeys: string[] } -export async function syncOrgRules(prisma: PrismaClient): Promise { +export async function syncOrgRules(dbClient: typeof db): Promise { const result: SyncResult = { created: 0, amended: 0, unchanged: 0, amendedKeys: [] } for (const seed of ORG_RULE_CATALOG) { - const existing = await prisma.houseRule.findUnique({ where: { key: seed.key } }) + const existing = await dbClient.query.houseRule.findFirst({ + where: eq(houseRule.key, seed.key), + }) if (!existing) { - await prisma.houseRule.create({ - data: { - scope: 'ORG', - key: seed.key, - category: seed.category, - title: seed.title, - body: seed.body, - delegation: seed.delegation, - status: 'ACTIVE', - version: 1, - }, + await dbClient.insert(houseRule).values({ + scope: 'ORG', + key: seed.key, + category: seed.category, + title: seed.title, + body: seed.body, + delegation: seed.delegation, + status: 'ACTIVE', + version: 1, }) result.created++ continue @@ -58,9 +59,9 @@ export async function syncOrgRules(prisma: PrismaClient): Promise { continue } - await prisma.houseRule.update({ - where: { id: existing.id }, - data: { + await dbClient + .update(houseRule) + .set({ category: seed.category, title: seed.title, body: seed.body, @@ -70,8 +71,8 @@ export async function syncOrgRules(prisma: PrismaClient): Promise { // rule does not change what a resident agreed to, so it must not // trigger a re-acknowledgement round for everyone. version: contentChanged ? existing.version + 1 : existing.version, - }, - }) + }) + .where(eq(houseRule.id, existing.id)) if (contentChanged) { result.amended++ diff --git a/src/lib/governance/voting.ts b/src/lib/governance/voting.ts index 3cbc4c9e..1362da63 100644 --- a/src/lib/governance/voting.ts +++ b/src/lib/governance/voting.ts @@ -6,7 +6,7 @@ * same words the staff see. No black-box outcomes (First Principle #3). */ -import type { VoteChoice, VoteThreshold } from '@prisma/client' +import type { VoteChoice, VoteThreshold } from '@/lib/db' // Relative rather than the usual '@/' alias: the demo seed reaches this module // through ts-node, which does not resolve tsconfig path aliases. import { DECISION_TIMING, THRESHOLD_APPROVAL_PERCENT, THRESHOLD_LABELS } from '../config/decisions' diff --git a/src/lib/housing/resident-ui.ts b/src/lib/housing/resident-ui.ts index 43258f6d..56bf8e11 100644 --- a/src/lib/housing/resident-ui.ts +++ b/src/lib/housing/resident-ui.ts @@ -1,4 +1,4 @@ -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import type { ResidentSummary } from '@/lib/types' /** @@ -41,7 +41,7 @@ export function toResidentUiSummary(r: Resident): ResidentUiSummary { displayName: r.displayName, ageRange: r.ageRange, gender: r.gender, - languages: r.languages, + languages: r.languages ?? [], socialStyle: r.socialStyle, sleepSchedule: r.sleepSchedule, smokingStatus: r.smokingStatus, diff --git a/src/lib/i18n/portal-surfaces.ts b/src/lib/i18n/portal-surfaces.ts index d73c3d22..2674ba82 100644 --- a/src/lib/i18n/portal-surfaces.ts +++ b/src/lib/i18n/portal-surfaces.ts @@ -1,4 +1,4 @@ -import type { ProfileVisibility } from '@prisma/client' +import type { ProfileVisibility } from '@/lib/db' import type { Translator } from './index' export function formatMarkPaidConfirm(t: Translator, amount: string, name: string): string { diff --git a/src/lib/matching/types.ts b/src/lib/matching/types.ts index 46cd2bc5..f8822bc7 100644 --- a/src/lib/matching/types.ts +++ b/src/lib/matching/types.ts @@ -2,7 +2,7 @@ * Types for the matching page and its extracted components */ -import type { Resident, HousingUnit, Placement, PlacementSpot } from '@prisma/client' +import type { Resident, HousingUnit, Placement, PlacementSpot } from '@/lib/db' import type { ApartmentProfile, ApartmentCompatibility, diff --git a/src/lib/messaging/queries.ts b/src/lib/messaging/queries.ts index 85a6293b..02601e20 100644 --- a/src/lib/messaging/queries.ts +++ b/src/lib/messaging/queries.ts @@ -1,4 +1,5 @@ -import { prisma } from '@/lib/db' +import { db, message as messageTable, messageThread } from '@/lib/db' +import { asc, desc, eq, inArray } from 'drizzle-orm' import { RESIDENT_NAME_SELECT } from '@/lib/utils/resident-name' import { messageAuthor, @@ -22,28 +23,48 @@ const MESSAGE_SELECT = { readAt: true, } as const +/** The message columns as insert-returning shape — mirrors MESSAGE_SELECT. */ +const MESSAGE_RETURNING = { + id: messageTable.id, + authorResidentId: messageTable.authorResidentId, + authorUserId: messageTable.authorUserId, + body: messageTable.body, + createdAt: messageTable.createdAt, + readAt: messageTable.readAt, +} as const + /** * The resident's thread, created on first use. * - * Upsert rather than "find, then create if missing": two requests arriving - * together — a resident sending while staff open the thread — would otherwise - * both find nothing and both insert, and the unique index would turn the loser - * into a 500 on an ordinary action. + * Insert-or-skip rather than "find, then create if missing": two requests + * arriving together — a resident sending while staff open the thread — would + * otherwise both find nothing and both insert, and the unique index would turn + * the loser into a 500 on an ordinary action. */ export async function getOrCreateThread(residentId: string) { - return prisma.messageThread.upsert({ - where: { residentId }, - create: { residentId }, - update: {}, - select: { id: true, residentId: true }, + const [created] = await db + .insert(messageThread) + .values({ residentId }) + .onConflictDoNothing({ target: messageThread.residentId }) + .returning({ id: messageThread.id, residentId: messageThread.residentId }) + if (created) return created + + const existing = await db.query.messageThread.findFirst({ + where: eq(messageThread.residentId, residentId), + columns: { id: true, residentId: true }, }) + if (!existing) { + // Unreachable unless the thread was deleted between the two statements. + throw new Error(`Message thread for resident ${residentId} vanished during creation`) + } + return existing } export async function loadThreadMessages(threadId: string): Promise { - return prisma.message.findMany({ - where: { threadId }, - orderBy: { createdAt: 'asc' }, - select: MESSAGE_SELECT, + return db.query.message.findMany({ + where: eq(messageTable.threadId, threadId), + orderBy: [asc(messageTable.createdAt)], + columns: MESSAGE_SELECT, }) } @@ -63,18 +84,18 @@ export async function appendMessage({ party: MessageParty body: string }) { - return prisma.$transaction(async (tx) => { - const message = await tx.message.create({ - data: { threadId, body: body.trim(), ...messageAuthor(party) }, - select: MESSAGE_SELECT, - }) + return db.transaction(async (tx) => { + const [message] = await tx + .insert(messageTable) + .values({ threadId, body: body.trim(), ...messageAuthor(party) }) + .returning(MESSAGE_RETURNING) - await tx.messageThread.update({ - where: { id: threadId }, - data: { updatedAt: new Date() }, - }) + await tx + .update(messageThread) + .set({ updatedAt: new Date() }) + .where(eq(messageThread.id, threadId)) - return message + return message! }) } @@ -84,18 +105,19 @@ export async function markThreadRead(threadId: string, party: MessageParty): Pro const ids = messagesToMarkRead(messages, party) if (ids.length === 0) return 0 - const result = await prisma.message.updateMany({ - where: { id: { in: ids } }, - data: { readAt: new Date() }, - }) - return result.count + const updated = await db + .update(messageTable) + .set({ readAt: new Date() }) + .where(inArray(messageTable.id, ids)) + .returning({ id: messageTable.id }) + return updated.length } /** How many messages are waiting for this resident. Drives the portal badge. */ export async function residentUnreadCount(residentId: string): Promise { - const thread = await prisma.messageThread.findUnique({ - where: { residentId }, - select: { id: true }, + const thread = await db.query.messageThread.findFirst({ + where: eq(messageThread.residentId, residentId), + columns: { id: true }, }) if (!thread) return 0 @@ -111,13 +133,12 @@ export async function residentUnreadCount(residentId: string): Promise { * sorting by recency buries exactly the person who has been waiting longest. */ export async function staffInbox() { - const threads = await prisma.messageThread.findMany({ - orderBy: { updatedAt: 'desc' }, - select: { - id: true, - updatedAt: true, - resident: { select: RESIDENT_NAME_SELECT }, - messages: { orderBy: { createdAt: 'asc' }, select: MESSAGE_SELECT }, + const threads = await db.query.messageThread.findMany({ + orderBy: [desc(messageThread.updatedAt)], + columns: { id: true, updatedAt: true }, + with: { + resident: { columns: RESIDENT_NAME_SELECT }, + messages: { orderBy: [asc(messageTable.createdAt)], columns: MESSAGE_SELECT }, }, }) diff --git a/src/lib/portal-auth.ts b/src/lib/portal-auth.ts index 0c1687a0..4e07b6f9 100644 --- a/src/lib/portal-auth.ts +++ b/src/lib/portal-auth.ts @@ -9,7 +9,8 @@ import { cookies } from 'next/headers' import { redirect } from 'next/navigation' -import { prisma } from '@/lib/db' +import { db, resident, placement } from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { RESIDENT_COOKIE, RESIDENT_COOKIE_MAX_AGE_SECONDS } from '@/lib/auth/constants' // Re-export so existing call sites continue to import from '@/lib/portal-auth'. @@ -68,11 +69,12 @@ export async function getPortalResident(): Promise<{ id: string; code: string } const residentCode = cookieStore.get(RESIDENT_COOKIE)?.value if (!residentCode) return null - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { id: true, code: true }, + const row = await db.query.resident.findFirst({ + where: eq(resident.code, residentCode), + columns: { id: true, code: true }, }) - return resident + // Callers expect Prisma's null, not drizzle's undefined. + return row ?? null } export interface UnitMember { @@ -85,19 +87,30 @@ export interface UnitMember { /** Active members of a housing unit, shaped for display (never includes photo bytes). */ export async function getActiveUnitMembers(housingUnitId: string): Promise { - const placements = await prisma.placement.findMany({ - where: { housingUnitId, status: 'ACTIVE' }, - select: { + const placements = (await db.query.placement.findMany({ + where: and(eq(placement.housingUnitId, housingUnitId), eq(placement.status, 'ACTIVE')), + columns: {}, + with: { resident: { - select: { + columns: { id: true, code: true, displayName: true, - photo: { select: { updatedAt: true } }, }, + with: { photo: { columns: { updatedAt: true } } }, }, }, - }) + // Cast: the relational result type is poisoned by the schema.ts + // circular-reference bug (placement is implicitly `any`); at runtime each + // row carries exactly one resident with an optional photo. + })) as { + resident: { + id: string + code: string + displayName: string | null + photo: { updatedAt: Date } | null + } + }[] return placements.map((p) => ({ id: p.resident.id, code: p.resident.code, @@ -116,26 +129,25 @@ export async function getPortalAuth(): Promise { if (!residentCode) return null - const resident = await prisma.resident.findUnique({ - where: { code: residentCode }, - select: { - id: true, - code: true, + const row = await db.query.resident.findFirst({ + where: eq(resident.code, residentCode), + columns: { id: true, code: true }, + with: { placements: { - where: { status: 'ACTIVE' }, - select: { id: true, housingUnitId: true }, - take: 1, + where: eq(placement.status, 'ACTIVE'), + columns: { id: true, housingUnitId: true }, + limit: 1, }, }, }) - if (!resident) return null + if (!row) return null - const placement = resident.placements[0] - if (!placement) return null + const active = row.placements[0] + if (!active) return null return { - resident: { id: resident.id, code: resident.code }, - placement: { id: placement.id, housingUnitId: placement.housingUnitId }, + resident: { id: row.id, code: row.code }, + placement: { id: active.id, housingUnitId: active.housingUnitId }, } } diff --git a/src/lib/privacy/profile-visibility.ts b/src/lib/privacy/profile-visibility.ts index 04d68a3c..150bc5b4 100644 --- a/src/lib/privacy/profile-visibility.ts +++ b/src/lib/privacy/profile-visibility.ts @@ -1,4 +1,4 @@ -import type { ProfileVisibility } from '@prisma/client' +import type { ProfileVisibility } from '@/lib/db' /** * Who may see a resident's self-chosen profile — SSOT. diff --git a/src/lib/reports/resident-reports.ts b/src/lib/reports/resident-reports.ts index d8beb110..e4202fae 100644 --- a/src/lib/reports/resident-reports.ts +++ b/src/lib/reports/resident-reports.ts @@ -9,7 +9,7 @@ * answer at all. */ -import type { MaintenanceStatus } from '@prisma/client' +import type { MaintenanceStatus } from '@/lib/db' import { INCIDENT_TYPE_LABELS, MAINTENANCE_CATEGORY_LABELS, getLabel } from '@/lib/constants' /** diff --git a/src/lib/reports/routing.ts b/src/lib/reports/routing.ts index ccbe9326..f063e45c 100644 --- a/src/lib/reports/routing.ts +++ b/src/lib/reports/routing.ts @@ -18,7 +18,7 @@ import type { IncidentType, MaintenanceCategory, MaintenancePriority, -} from '@prisma/client' +} from '@/lib/db' /** * The maintenance incident types, mapped onto the maintenance board's own diff --git a/src/lib/seed/integration-evidence.ts b/src/lib/seed/integration-evidence.ts index 5566ca65..21ea6a5d 100644 --- a/src/lib/seed/integration-evidence.ts +++ b/src/lib/seed/integration-evidence.ts @@ -32,11 +32,22 @@ * Relative-import-safe (no '@/' aliases): loaded through ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { and, asc, eq, inArray } from 'drizzle-orm' +import { + appointment, + careAssignment, + learningRecord, + placement, + resident as residentTable, + satisfactionCheckIn, + type db, +} from '../db' import type { LearningCategoryId, LearningKindId, LearningStatusId } from '../config/learning' import { LEARNING_PULSE_WINDOW_DAYS } from '../config/learning' import { BRAND } from '../config/brand' +type Db = typeof db + const DAY_MS = 24 * 60 * 60 * 1000 const daysAgo = (days: number) => new Date(Date.now() - days * DAY_MS) const daysAhead = (days: number) => new Date(Date.now() + days * DAY_MS) @@ -230,7 +241,7 @@ type RecordPayload = { * The evidence one resident carries, derived from who they are. * * Exported for the test: the derivation rules are the whole point of this - * module, and asserting them through a database round-trip would test Prisma + * module, and asserting them through a database round-trip would test the ORM * instead of the rules. */ export function evidenceForResident(resident: EvidenceProfile, index: number): RecordPayload[] { @@ -393,26 +404,28 @@ const APPOINTMENT_SCRIPT = [ const CARE_ROLES = ['HOUSING', 'SOCIAL', 'JOB', 'VOLUNTEERING'] as const export async function seedIntegrationEvidence( - prisma: PrismaClient, + dbc: Db, ctx: IntegrationSeedContext, ): Promise { - const residents = await prisma.resident.findMany({ - where: { id: { in: ctx.residentIds } }, - select: { - id: true, - languages: true, - ageRange: true, - choresContribution: true, - }, - // Sorted so the index — and therefore the whole world — is reproducible. - orderBy: { code: 'asc' }, - }) + const residents = ctx.residentIds.length + ? await dbc.query.resident.findMany({ + where: inArray(residentTable.id, ctx.residentIds), + columns: { + id: true, + languages: true, + ageRange: true, + choresContribution: true, + }, + // Sorted so the index — and therefore the whole world — is reproducible. + orderBy: [asc(residentTable.code)], + }) + : [] const payloads = residents.flatMap((resident, index) => evidenceForResident( { id: resident.id, - languages: resident.languages, + languages: resident.languages ?? [], ageRange: resident.ageRange, choresContribution: resident.choresContribution, }, @@ -421,23 +434,25 @@ export async function seedIntegrationEvidence( ) if (payloads.length > 0) { - await prisma.learningRecord.createMany({ data: payloads }) + await dbc.insert(learningRecord).values(payloads) } let careAssignments = 0 - if (ctx.staffId) { - const result = await prisma.careAssignment.createMany({ - data: residents.flatMap((resident) => - CARE_ROLES.map((role) => ({ - residentId: resident.id, - staffId: ctx.staffId as string, - role, - })), - ), + if (ctx.staffId && residents.length > 0) { + const result = await dbc + .insert(careAssignment) + .values( + residents.flatMap((resident) => + CARE_ROLES.map((role) => ({ + residentId: resident.id, + staffId: ctx.staffId as string, + role, + })), + ), + ) // The seed may run over a world that already has real assignments. - skipDuplicates: true, - }) - careAssignments = result.count + .onConflictDoNothing() + careAssignments = result.rowCount ?? 0 } // --------------------------------------------------------------------- @@ -452,65 +467,69 @@ export async function seedIntegrationEvidence( // The check-in hangs off a placement, so only a placed resident can carry // the completed half. Everyone still gets the scheduled one. - const placements = await prisma.placement.findMany({ - where: { residentId: { in: residents.map((r) => r.id) }, status: 'ACTIVE' }, - select: { id: true, residentId: true, startDate: true }, - }) + const placements = residents.length + ? await dbc.query.placement.findMany({ + where: and( + inArray( + placement.residentId, + residents.map((r) => r.id), + ), + eq(placement.status, 'ACTIVE'), + ), + columns: { id: true, residentId: true, startDate: true }, + }) + : [] const placementByResident = new Map(placements.map((p) => [p.residentId, p])) for (let index = 0; index < residents.length; index += 1) { const resident = residents[index] const script = APPOINTMENT_SCRIPT[index % APPOINTMENT_SCRIPT.length] - const upcoming = await prisma.appointment.create({ - data: { - residentId: resident.id, - staffId, - domain: script.domain, - title: script.next, - // A day out, so it is still upcoming however late in the day the - // visitor arrives — the demo world is rebuilt nightly. - startsAt: daysAhead(1), - status: 'SCHEDULED', - }, + await dbc.insert(appointment).values({ + residentId: resident.id, + staffId, + domain: script.domain, + title: script.next, + // A day out, so it is still upcoming however late in the day the + // visitor arrives — the demo world is rebuilt nightly. + startsAt: daysAhead(1), + status: 'SCHEDULED', }) appointments += 1 - void upcoming - const placement = placementByResident.get(resident.id) - if (!placement) continue + const residentPlacement = placementByResident.get(resident.id) + if (!residentPlacement) continue - const held = await prisma.appointment.create({ - data: { + const [held] = await dbc + .insert(appointment) + .values({ residentId: resident.id, staffId, domain: script.domain, title: script.past, startsAt: daysAgo(6 + (index % 5)), status: 'COMPLETED', - }, - }) + }) + .returning() appointments += 1 // The reading this product now produces: attached to the conversation it // came from, and attributed to the account that recorded it. A demo that // showed a score with neither would be demonstrating the old behaviour. - await prisma.satisfactionCheckIn.create({ - data: { - placementId: placement.id, - appointmentId: held.id, - checkInType: 'AD_HOC', - weekNumber: Math.max( - 0, - Math.floor((Date.now() - placement.startDate.getTime()) / (7 * DAY_MS)), - ), - // Deliberately not all 5s: an even record shows nothing, the same - // reason the seeded chore history is uneven. - overallSatisfaction: 3 + (index % 3), - concerns: index % 3 === 0 ? 'Sucht eine Anschlusslösung für den Winter.' : null, - collectedByUserId: staffId, - isAnonymous: false, - }, + await dbc.insert(satisfactionCheckIn).values({ + placementId: residentPlacement.id, + appointmentId: held.id, + checkInType: 'AD_HOC', + weekNumber: Math.max( + 0, + Math.floor((Date.now() - residentPlacement.startDate.getTime()) / (7 * DAY_MS)), + ), + // Deliberately not all 5s: an even record shows nothing, the same + // reason the seeded chore history is uneven. + overallSatisfaction: 3 + (index % 3), + concerns: index % 3 === 0 ? 'Sucht eine Anschlusslösung für den Winter.' : null, + collectedByUserId: staffId, + isAnonymous: false, }) } } diff --git a/src/lib/seed/opportunities.ts b/src/lib/seed/opportunities.ts index 699b7325..56cf8ecd 100644 --- a/src/lib/seed/opportunities.ts +++ b/src/lib/seed/opportunities.ts @@ -16,9 +16,16 @@ * Relative-import-safe (no '@/' aliases): loaded through plain ts-node. */ -import type { PrismaClient } from '@prisma/client' +import { + learningRecord, + opportunity as opportunityTable, + opportunityApplication, + type db, +} from '../db' import { evidenceForStartedApplication } from '../opportunities/pipeline' +type Db = typeof db + type Kind = 'VOLUNTEERING' | 'COMMUNITY_SERVICE' type Permit = 'NONE' | 'EMPLOYER_NOTIFIES' | 'PERMIT_REQUIRED' type Stage = 'INTERESTED' | 'APPLIED' | 'INTERVIEW' | 'ACCEPTED' | 'STARTED' | 'ENDED' | 'DECLINED' @@ -167,7 +174,7 @@ export interface OpportunitySeedSummary { } export async function seedOpportunities( - prisma: PrismaClient, + dbc: Db, ctx: OpportunitySeedContext, ): Promise { const now = ctx.now ?? new Date() @@ -181,8 +188,9 @@ export async function seedOpportunities( for (const template of TEMPLATES) { const { stages, ...columns } = template - const opportunity = await prisma.opportunity.create({ - data: { + const [opportunity] = await dbc + .insert(opportunityTable) + .values({ ...columns, // The draft listing is the one with no applicants — a board where // everything is published shows a filter that looks broken. @@ -190,8 +198,8 @@ export async function seedOpportunities( startsAt: daysAgo(60, now), createdByUserId: ctx.staffId, updatedByUserId: ctx.staffId, - }, - }) + }) + .returning() opportunities += 1 // One application per resident per listing is a unique constraint. With @@ -217,30 +225,29 @@ export async function seedOpportunities( // that does not exist. let learningRecordId: string | null = null if (stage === 'STARTED' || stage === 'ENDED') { - const record = await prisma.learningRecord.create({ - data: { + const [record] = await dbc + .insert(learningRecord) + .values({ residentId, ...evidenceForStartedApplication(opportunity, stageChangedAt), ...(stage === 'ENDED' ? { status: 'COMPLETED' as const, completedAt: daysAgo(7, now), hours: 48 } : {}), - }, - }) + }) + .returning() learningRecordId = record.id evidenceRecords += 1 } - await prisma.opportunityApplication.create({ - data: { - opportunityId: opportunity.id, - residentId, - stage, - stageChangedAt, - createdAt: daysAgo(STAGE_AGE_DAYS[stage] + 4, now), - createdBy: stage === 'INTERESTED' ? 'RESIDENT' : 'STAFF', - supportedByUserId: ctx.staffId, - learningRecordId, - }, + await dbc.insert(opportunityApplication).values({ + opportunityId: opportunity.id, + residentId, + stage, + stageChangedAt, + createdAt: daysAgo(STAGE_AGE_DAYS[stage] + 4, now), + createdBy: stage === 'INTERESTED' ? 'RESIDENT' : 'STAFF', + supportedByUserId: ctx.staffId, + learningRecordId, }) applications += 1 } diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 5b10afb7..b0e4cd2e 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -7,7 +7,7 @@ * @see prisma/schema.prisma (source of truth) */ -import type { Resident, PlacementSpot, HousingUnit } from '@prisma/client' +import type { Resident, PlacementSpot, HousingUnit } from '@/lib/db' // ============================================================================= // RESIDENT SUBSETS @@ -30,7 +30,12 @@ export type ResidentSummary = Pick< | 'noiseTolerance' | 'cleanlinessPractice' | 'privacyNeed' -> +> & { + // The DB column is a nullable TEXT[] (Prisma typed it string[], writing [] + // on create). The UI contract stays non-null; toResidentUiSummary and every + // other projection normalises `?? []` at the boundary. + languages: string[] +} /** Resident fields needed for apartment profile calculations */ export type ResidentHouseholdProfile = Pick< @@ -76,4 +81,4 @@ export interface UnitWithSpots { // RE-EXPORTS for convenience // ============================================================================= -export type { Resident, PlacementSpot, HousingUnit } from '@prisma/client' +export type { Resident, PlacementSpot, HousingUnit } from '@/lib/db' diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index c57fc8d4..645907ca 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -39,7 +39,7 @@ import type { MaintenancePriority, MaintenanceStatus, CheckInType, -} from '@prisma/client' +} from '@/lib/db' import { RESIDENT_FACTORS } from '@/lib/config/resident-factors' import { INCIDENT_CATEGORY_LABELS, @@ -648,7 +648,7 @@ import type { HouseholdTaskType, HouseholdTaskCategory, HouseholdTaskPriority, -} from '@prisma/client' +} from '@/lib/db' export const HouseholdTaskTypeSchema = enumFromKeys(TASK_TYPE_LABELS) export const HouseholdTaskCategorySchema = enumFromKeys(TASK_CATEGORY_LABELS) diff --git a/src/lib/vulnerability/index.ts b/src/lib/vulnerability/index.ts index 708db386..a3ec75a5 100644 --- a/src/lib/vulnerability/index.ts +++ b/src/lib/vulnerability/index.ts @@ -48,7 +48,7 @@ import type { MobilityNeed, RoomSharingStatus, SupportLevel, -} from '@prisma/client' +} from '@/lib/db' /** * The subset of a resident this reads. Deliberately narrow and explicit: the From 36e1c9ca7072cc549e80d6978b62b6e53bff9cdf Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:30:32 +0200 Subject: [PATCH 3/5] refactor(db): retarget every test mock to the Drizzle surface; port seeds; delete Prisma Tests (62 files): jest.mock('@/lib/db') now fakes `db` (query API + chainable insert/update/delete builders + transactions + execute) instead of the Prisma model map, with jest.requireActual keeping tables/enums/ helpers real. Assertions moved from `{data}`/`{where}` object equality to `.values()` payloads and real drizzle where-expressions - discriminating power kept, nothing weakened to anything(), no test deleted or skipped. New shared helper src/test-utils/drizzle-where.ts (eqParts/whereParts/ sqlText) keeps mock dispatch DRY. jest transformIgnorePatterns admits the ESM-only @paralleldrive/cuid2 + @noble/hashes. Seeds: prisma/seed*.ts -> scripts/db/ (git mv), converted and PROVEN by execution against a scratch Postgres: db:migrate + db:seed + db:seed:admin all exit 0, 25 residents / 18 placements / live compatibility scores, and a second run over the populated DB confirms the FK deletion order and idempotent governance path. Prisma is now GONE: prisma/ (schema + 30 migrations) deleted, @prisma/ client + prisma removed from package.json, no remaining import outside historical comments. The guard tests that used to regex schema.prisma now read the pgEnum objects at runtime. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn --- jest.config.ts | 8 +- package-lock.json | 90 - package.json | 2 - prisma/migrations/0_init/migration.sql | 679 ---- .../migration.sql | 2 - .../migration.sql | 28 - .../migration.sql | 15 - .../20260225_unified_code_auth/migration.sql | 21 - .../migration.sql | 47 - .../migration.sql | 19 - .../migration.sql | 240 -- .../migration.sql | 18 - .../migration.sql | 95 - .../migration.sql | 43 - .../migration.sql | 98 - .../migration.sql | 16 - .../migration.sql | 54 - .../migration.sql | 11 - .../migration.sql | 54 - .../migration.sql | 27 - .../migration.sql | 51 - .../migration.sql | 115 - .../migration.sql | 96 - .../migration.sql | 36 - .../migration.sql | 31 - .../migration.sql | 13 - .../migration.sql | 18 - .../migration.sql | 46 - .../migration.sql | 33 - .../migration.sql | 28 - .../migration.sql | 28 - .../migration.sql | 52 - prisma/migrations/migration_lock.toml | 3 - prisma/schema.prisma | 2408 ------------- prisma/seed-demo.ts | 26 - prisma/seed.ts | 2984 ----------------- {prisma => scripts/db}/real/aoz-team.ts | 2 +- .../db}/real/witikonerstrasse-458.ts | 2 +- {prisma => scripts/db}/scoring-helper.ts | 4 +- {prisma => scripts/db}/seed-admin.ts | 56 +- scripts/db/seed-demo.ts | 22 + {prisma => scripts/db}/seed-real.ts | 90 +- scripts/db/seed.ts | 2754 +++++++++++++++ scripts/maintenance/ensure-aoz-team.ts | 4 +- src/app/api/__tests__/staff-chores.test.ts | 33 +- .../api/auth/__tests__/auth-routes.test.ts | 115 +- src/app/api/auth/__tests__/demo.test.ts | 24 +- src/app/api/auth/__tests__/invite.test.ts | 96 +- .../provisioning-least-privilege.test.ts | 69 +- .../api/cron/__tests__/notifications.test.ts | 38 +- src/app/api/cron/__tests__/reset-demo.test.ts | 20 +- .../export/__tests__/export-routes.test.ts | 15 +- .../import/__tests__/import-routes.test.ts | 117 +- src/app/api/portal/__tests__/chores.test.ts | 553 ++- src/app/api/portal/__tests__/expenses.test.ts | 76 +- .../api/portal/__tests__/preferences.test.ts | 62 +- src/app/api/portal/__tests__/profile.test.ts | 132 +- src/app/api/portal/__tests__/report.test.ts | 151 +- .../api/portal/__tests__/satisfaction.test.ts | 97 +- src/app/api/portal/__tests__/transfer.test.ts | 71 +- .../matching/__tests__/MatchCard.test.tsx | 2 +- .../opportunities/OpportunityFormFields.tsx | 2 +- src/lib/__tests__/audit.test.ts | 59 +- src/lib/__tests__/portal-auth.test.ts | 70 +- src/lib/__tests__/resident-ui-ssot.test.ts | 2 +- src/lib/__tests__/scoring-ssot.test.ts | 25 +- .../__tests__/appointment-requests.test.ts | 97 +- .../care-appointment-checkin.test.ts | 88 +- src/lib/actions/__tests__/config.test.ts | 125 +- src/lib/actions/__tests__/housing.test.ts | 104 +- src/lib/actions/__tests__/incidents.test.ts | 79 +- src/lib/actions/__tests__/maintenance.test.ts | 178 +- src/lib/actions/__tests__/marketplace.test.ts | 145 +- src/lib/actions/__tests__/matching.test.ts | 100 +- .../__tests__/opportunities-portal.test.ts | 98 +- src/lib/actions/__tests__/placements.test.ts | 146 +- src/lib/actions/__tests__/residents.test.ts | 133 +- .../actions/__tests__/satisfaction.test.ts | 152 +- src/lib/actions/__tests__/spots.test.ts | 211 +- src/lib/actions/__tests__/transfers.test.ts | 101 +- .../__tests__/algorithm-accuracy.test.ts | 9 +- .../analytics/__tests__/mission-kpis.test.ts | 90 +- .../analytics/__tests__/unit-metrics.test.ts | 175 +- src/lib/auth/__tests__/account.test.ts | 179 +- .../auth/__tests__/admin-page-guards.test.ts | 4 +- .../auth/__tests__/complaint-boundary.test.ts | 6 +- .../auth/__tests__/household-aoz-gate.test.ts | 22 +- src/lib/auth/__tests__/household.test.ts | 98 +- src/lib/auth/__tests__/login-by-code.test.ts | 35 +- .../staff-row-states-its-reach.test.ts | 56 +- src/lib/auth/__tests__/tokens.test.ts | 66 +- .../compatibility/__tests__/convert.test.ts | 2 +- .../__tests__/placement-scores.test.ts | 2 +- .../__tests__/appointment-statuses.test.ts | 44 +- .../__tests__/decision-mode-voting.test.ts | 2 +- src/lib/config/__tests__/infra-ssot.test.ts | 8 +- src/lib/config/care.ts | 2 +- src/lib/config/conflict-resolution.ts | 7 +- src/lib/config/housing-factors.ts | 4 +- src/lib/config/marketplace.ts | 2 +- src/lib/config/thresholds.ts | 2 +- .../__tests__/opportunities-board.test.ts | 39 +- src/lib/demo/__tests__/reset.test.ts | 103 +- src/lib/demo/__tests__/scoped-reset.test.ts | 207 +- src/lib/demo/__tests__/seed-data.test.ts | 225 +- src/lib/demo/seed-data.ts | 32 +- .../governance/__tests__/lifecycle.test.ts | 196 +- .../__tests__/sync-org-rules.test.ts | 60 +- src/lib/governance/__tests__/voting.test.ts | 2 +- .../__tests__/opportunity-kinds.test.ts | 46 +- .../__tests__/work-permit-gate.test.ts | 41 +- .../__tests__/profile-visibility.test.ts | 2 +- .../__tests__/integration-evidence.test.ts | 70 +- src/lib/validation/schemas.ts | 6 +- .../__tests__/vulnerability.test.ts | 7 +- src/test-utils/drizzle-where.ts | 59 + tests/maintenance.spec.ts | 2 +- tests/matching-flow.spec.ts | 2 +- tests/portal.spec.ts | 2 +- tests/resident-detail.spec.ts | 2 +- 120 files changed, 5871 insertions(+), 10102 deletions(-) delete mode 100644 prisma/migrations/0_init/migration.sql delete mode 100644 prisma/migrations/20260223000000_add_preferences_completed_at/migration.sql delete mode 100644 prisma/migrations/20260223_add_transfer_requests/migration.sql delete mode 100644 prisma/migrations/20260223_simplify_roles_admin_only/migration.sql delete mode 100644 prisma/migrations/20260225_unified_code_auth/migration.sql delete mode 100644 prisma/migrations/20260518093000_add_activities/migration.sql delete mode 100644 prisma/migrations/20260729000000_add_mediation_minutes_and_system_config/migration.sql delete mode 100644 prisma/migrations/20260806130000_add_house_rules_governance/migration.sql delete mode 100644 prisma/migrations/20260806140000_split_cleanliness_dimensions/migration.sql delete mode 100644 prisma/migrations/20260813000000_add_expenses_and_resident_profiles/migration.sql delete mode 100644 prisma/migrations/20260813210000_modern_auth_email_password/migration.sql delete mode 100644 prisma/migrations/20260814000000_accounts_link_identities/migration.sql delete mode 100644 prisma/migrations/20260815000000_chore_checklist_rotation_balance/migration.sql delete mode 100644 prisma/migrations/20260816100000_roles_learning_building/migration.sql delete mode 100644 prisma/migrations/20260816120000_resident_profile_visibility/migration.sql delete mode 100644 prisma/migrations/20260816140000_resident_staff_messaging/migration.sql delete mode 100644 prisma/migrations/20260817100000_care_team_learning_kinds/migration.sql delete mode 100644 prisma/migrations/20260817120000_care_appointments_attributes/migration.sql delete mode 100644 prisma/migrations/20260818091141_add_volunteering_role_marketplace_events/migration.sql delete mode 100644 prisma/migrations/20260824225746_opportunities_and_applications/migration.sql delete mode 100644 prisma/migrations/20260825120000_marketplace_goods_and_services/migration.sql delete mode 100644 prisma/migrations/20260828100000_checkin_belongs_to_an_appointment/migration.sql delete mode 100644 prisma/migrations/20260828120000_staff_role_defaults_to_least_privilege/migration.sql delete mode 100644 prisma/migrations/20260828140000_employment_and_internship_kinds/migration.sql delete mode 100644 prisma/migrations/20260828170000_resident_career_documents/migration.sql delete mode 100644 prisma/migrations/20260829080000_residents_can_request_appointments/migration.sql delete mode 100644 prisma/migrations/20260829100000_staff_scope_separate_from_role/migration.sql delete mode 100644 prisma/migrations/20260901020000_living_skills_support/migration.sql delete mode 100644 prisma/migrations/20260901090000_complaints_about_the_organisation/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml delete mode 100644 prisma/schema.prisma delete mode 100644 prisma/seed-demo.ts delete mode 100644 prisma/seed.ts rename {prisma => scripts/db}/real/aoz-team.ts (96%) rename {prisma => scripts/db}/real/witikonerstrasse-458.ts (93%) rename {prisma => scripts/db}/scoring-helper.ts (94%) rename {prisma => scripts/db}/seed-admin.ts (69%) create mode 100644 scripts/db/seed-demo.ts rename {prisma => scripts/db}/seed-real.ts (65%) create mode 100644 scripts/db/seed.ts create mode 100644 src/test-utils/drizzle-where.ts diff --git a/jest.config.ts b/jest.config.ts index 8f7591bd..213d08e9 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -21,7 +21,9 @@ const config: Config = { // ai-kit is ESM-only ("type": "module", no require condition), like // every other entry here. Jest runs CJS, so it must be transformed // rather than required. - transformIgnorePatterns: ['node_modules/(?!(jose|@fleet/ai-forms|bip-kit|ai-kit))'], + transformIgnorePatterns: [ + 'node_modules/(?!(@paralleldrive/cuid2|@noble/hashes|jose|@fleet/ai-forms|bip-kit|ai-kit))', + ], transform: { '^.+\\.tsx?$': 'ts-jest', '^.+\\.js$': 'ts-jest', @@ -44,7 +46,9 @@ const config: Config = { // ai-kit is ESM-only ("type": "module", no require condition), like // every other entry here. Jest runs CJS, so it must be transformed // rather than required. - transformIgnorePatterns: ['node_modules/(?!(jose|@fleet/ai-forms|bip-kit|ai-kit))'], + transformIgnorePatterns: [ + 'node_modules/(?!(@paralleldrive/cuid2|@noble/hashes|jose|@fleet/ai-forms|bip-kit|ai-kit))', + ], transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: { jsx: 'react-jsx' } }], '^.+\\.js$': ['ts-jest', { tsconfig: { allowJs: true, jsx: 'react-jsx' } }], diff --git a/package-lock.json b/package-lock.json index 6e84ef84..9582f9b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", "@paralleldrive/cuid2": "^3.3.0", - "@prisma/client": "^5.17.0", "@sentry/nextjs": "^10.71.0", "@types/bcryptjs": "^2.4.6", "ai-kit": "github:bitbaum/ai-kit#v0.6.2", @@ -49,7 +48,6 @@ "jest-environment-jsdom": "^30.4.1", "postcss": "^8.5.26", "prettier": "3.9.6", - "prisma": "^5.17.0", "ts-jest": "^29.4.12", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", @@ -4021,74 +4019,6 @@ "node": ">=20" } }, - "node_modules/@prisma/client": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", - "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.13" - }, - "peerDependencies": { - "prisma": "*" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - } - } - }, - "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "5.22.0" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", @@ -12803,26 +12733,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/engines": "5.22.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=16.13" - }, - "optionalDependencies": { - "fsevents": "2.3.3" - } - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", diff --git a/package.json b/package.json index 81c94eeb..ec5c9a59 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "dependencies": { "@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.0", "@paralleldrive/cuid2": "^3.3.0", - "@prisma/client": "^5.17.0", "@sentry/nextjs": "^10.71.0", "@types/bcryptjs": "^2.4.6", "ai-kit": "github:bitbaum/ai-kit#v0.6.2", @@ -66,7 +65,6 @@ "jest-environment-jsdom": "^30.4.1", "postcss": "^8.5.26", "prettier": "3.9.6", - "prisma": "^5.17.0", "ts-jest": "^29.4.12", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql deleted file mode 100644 index 31cd170c..00000000 --- a/prisma/migrations/0_init/migration.sql +++ /dev/null @@ -1,679 +0,0 @@ --- CreateEnum -CREATE TYPE "AgeRange" AS ENUM ('YOUNG_ADULT', 'ADULT', 'MIDDLE_AGED', 'SENIOR'); - --- CreateEnum -CREATE TYPE "Gender" AS ENUM ('MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_SAY'); - --- CreateEnum -CREATE TYPE "FamilyStatus" AS ENUM ('SINGLE', 'COUPLE', 'FAMILY_WITH_CHILDREN', 'SINGLE_PARENT'); - --- CreateEnum -CREATE TYPE "SleepSchedule" AS ENUM ('EARLY_BIRD', 'STANDARD', 'NIGHT_OWL', 'IRREGULAR'); - --- CreateEnum -CREATE TYPE "SocialStyle" AS ENUM ('INTROVERTED', 'MODERATE', 'EXTROVERTED'); - --- CreateEnum -CREATE TYPE "SmokingStatus" AS ENUM ('NON_SMOKER', 'OUTDOOR_SMOKER', 'INDOOR_SMOKER'); - --- CreateEnum -CREATE TYPE "MobilityNeed" AS ENUM ('NONE', 'GROUND_FLOOR', 'WHEELCHAIR'); - --- CreateEnum -CREATE TYPE "ResidentStatus" AS ENUM ('ACTIVE', 'PLACED', 'TRANSFERRED', 'EXITED'); - --- CreateEnum -CREATE TYPE "MedicalDocType" AS ENUM ('PRIVATE_ROOM', 'STUDIO', 'BOTH'); - --- CreateEnum -CREATE TYPE "RoomSharingStatus" AS ENUM ('CAN_SHARE', 'PREFERS_PRIVATE', 'NEEDS_PRIVATE'); - --- CreateEnum -CREATE TYPE "SupportLevel" AS ENUM ('STANDARD', 'ELEVATED', 'INTENSIVE'); - --- CreateEnum -CREATE TYPE "RecyclingKnowledge" AS ENUM ('NONE', 'BASIC', 'GOOD'); - --- CreateEnum -CREATE TYPE "ConflictStyle" AS ENUM ('AVOIDANT', 'COOPERATIVE', 'DIRECT'); - --- CreateEnum -CREATE TYPE "HousingStatus" AS ENUM ('AVAILABLE', 'FULL', 'MAINTENANCE', 'CLOSED'); - --- CreateEnum -CREATE TYPE "SpotType" AS ENUM ('BED', 'PRIVATE_ROOM', 'STUDIO', 'ROOM'); - --- CreateEnum -CREATE TYPE "SpotStatus" AS ENUM ('AVAILABLE', 'OCCUPIED', 'MAINTENANCE', 'CLOSED'); - --- CreateEnum -CREATE TYPE "PlacementStatus" AS ENUM ('ACTIVE', 'ENDED', 'TRANSFERRED'); - --- CreateEnum -CREATE TYPE "EndReason" AS ENUM ('NATURAL', 'CONFLICT', 'REQUEST', 'CAPACITY', 'UPGRADE', 'OTHER'); - --- CreateEnum -CREATE TYPE "FollowUpPriority" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT'); - --- CreateEnum -CREATE TYPE "InvolvementRole" AS ENUM ('INVOLVED', 'WITNESS', 'MEDIATOR'); - --- CreateEnum -CREATE TYPE "IncidentType" AS ENUM ('NOISE_COMPLAINT', 'CLEANLINESS_DISPUTE', 'PERSONAL_CONFLICT', 'CULTURAL_FRICTION', 'SPACE_DISPUTE', 'SCHEDULE_CONFLICT', 'SAFETY_CONCERN', 'PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY_SYSTEM', 'GENERAL_MAINTENANCE', 'LOW_SATISFACTION', 'OTHER'); - --- CreateEnum -CREATE TYPE "IncidentCategory" AS ENUM ('INTERPERSONAL', 'MAINTENANCE', 'SAFETY', 'WELLBEING'); - --- CreateEnum -CREATE TYPE "IncidentSeverity" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL'); - --- CreateEnum -CREATE TYPE "CheckInType" AS ENUM ('INITIAL', 'REGULAR', 'AD_HOC', 'EXIT'); - --- CreateEnum -CREATE TYPE "StaffRole" AS ENUM ('ADMIN', 'CASE_WORKER', 'VIEWER'); - --- CreateEnum -CREATE TYPE "MaintenanceCategory" AS ENUM ('PLUMBING', 'ELECTRICAL', 'HEATING_COOLING', 'APPLIANCE', 'STRUCTURAL', 'PEST_CONTROL', 'SECURITY', 'CLEANING', 'EXTERIOR', 'OTHER'); - --- CreateEnum -CREATE TYPE "MaintenancePriority" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT'); - --- CreateEnum -CREATE TYPE "MaintenanceStatus" AS ENUM ('OPEN', 'ASSIGNED', 'IN_PROGRESS', 'ON_HOLD', 'COMPLETED', 'CANCELLED'); - --- CreateEnum -CREATE TYPE "HouseholdTaskType" AS ENUM ('ONE_TIME', 'RECURRING_SCHEDULED', 'RECURRING_AS_NEEDED'); - --- CreateEnum -CREATE TYPE "HouseholdTaskCategory" AS ENUM ('CLEANING', 'SHOPPING', 'MAINTENANCE', 'COOKING', 'TRASH', 'OTHER'); - --- CreateEnum -CREATE TYPE "HouseholdTaskPriority" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT'); - --- CreateEnum -CREATE TYPE "HouseholdTaskStatus" AS ENUM ('IDLE', 'NEEDS_ATTENTION', 'REQUESTED', 'IN_PROGRESS'); - --- CreateEnum -CREATE TYPE "TaskRequestStatus" AS ENUM ('PENDING', 'ACCEPTED', 'DECLINED', 'COMPLETED'); - --- CreateTable -CREATE TABLE "Resident" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "code" TEXT NOT NULL, - "ageRange" "AgeRange" NOT NULL, - "gender" "Gender" NOT NULL, - "familyStatus" "FamilyStatus" NOT NULL, - "sleepSchedule" "SleepSchedule" NOT NULL, - "noiseTolerance" INTEGER NOT NULL, - "cleanlinessLevel" INTEGER NOT NULL, - "guestTolerance" INTEGER NOT NULL DEFAULT 3, - "socialStyle" "SocialStyle" NOT NULL, - "languages" TEXT[], - "culturalRegion" TEXT, - "conflictStyle" "ConflictStyle" NOT NULL DEFAULT 'COOPERATIVE', - "smokingStatus" "SmokingStatus" NOT NULL, - "dietaryNeeds" TEXT[], - "mobilityNeeds" "MobilityNeed" NOT NULL, - "medicalEquipment" BOOLEAN NOT NULL DEFAULT false, - "petTolerance" BOOLEAN NOT NULL DEFAULT true, - "sharedBathroom" BOOLEAN NOT NULL DEFAULT true, - "sharedKitchen" BOOLEAN NOT NULL DEFAULT true, - "privacyNeed" INTEGER NOT NULL, - "choresContribution" INTEGER NOT NULL DEFAULT 3, - "recyclingKnowledge" "RecyclingKnowledge" NOT NULL DEFAULT 'NONE', - "roomSharingStatus" "RoomSharingStatus" NOT NULL DEFAULT 'CAN_SHARE', - "hasNightDisturbances" BOOLEAN NOT NULL DEFAULT false, - "needsQuietEnvironment" BOOLEAN NOT NULL DEFAULT false, - "hasSleepEquipment" BOOLEAN NOT NULL DEFAULT false, - "supportLevel" "SupportLevel" NOT NULL DEFAULT 'STANDARD', - "roommatePreferences" TEXT, - "status" "ResidentStatus" NOT NULL DEFAULT 'ACTIVE', - "notes" TEXT, - "hasMedicalDocumentation" BOOLEAN NOT NULL DEFAULT false, - "medicalDocType" "MedicalDocType", - "medicalDocDate" TIMESTAMP(3), - "medicalDocNotes" TEXT, - - CONSTRAINT "Resident_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "HousingUnit" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "code" TEXT NOT NULL, - "address" TEXT NOT NULL, - "totalBeds" INTEGER NOT NULL, - "totalRooms" INTEGER NOT NULL, - "sharedRooms" INTEGER NOT NULL, - "privateRooms" INTEGER NOT NULL, - "sharedBathrooms" INTEGER NOT NULL, - "privateBathrooms" INTEGER NOT NULL, - "sharedKitchen" BOOLEAN NOT NULL DEFAULT true, - "privateKitchen" BOOLEAN NOT NULL DEFAULT false, - "groundFloor" BOOLEAN NOT NULL DEFAULT false, - "wheelchairAccess" BOOLEAN NOT NULL DEFAULT false, - "elevator" BOOLEAN NOT NULL DEFAULT false, - "smokingAllowed" BOOLEAN NOT NULL DEFAULT false, - "petsAllowed" BOOLEAN NOT NULL DEFAULT false, - "quietHours" TEXT, - "nearPublicTransport" BOOLEAN NOT NULL DEFAULT true, - "nearHealthServices" BOOLEAN NOT NULL DEFAULT false, - "nearSchools" BOOLEAN NOT NULL DEFAULT false, - "status" "HousingStatus" NOT NULL DEFAULT 'AVAILABLE', - "notes" TEXT, - - CONSTRAINT "HousingUnit_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "PlacementSpot" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "code" TEXT NOT NULL, - "label" TEXT, - "type" "SpotType" NOT NULL, - "parentSpotId" TEXT, - "squareMeters" DOUBLE PRECISION, - "floor" INTEGER, - "hasPrivateBathroom" BOOLEAN NOT NULL DEFAULT false, - "hasPrivateKitchen" BOOLEAN NOT NULL DEFAULT false, - "hasPrivateToilet" BOOLEAN NOT NULL DEFAULT false, - "capacity" INTEGER NOT NULL DEFAULT 1, - "requiresMedicalDocs" BOOLEAN NOT NULL DEFAULT false, - "status" "SpotStatus" NOT NULL DEFAULT 'AVAILABLE', - "notes" TEXT, - - CONSTRAINT "PlacementSpot_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Placement" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "housingUnitId" TEXT NOT NULL, - "spotId" TEXT, - "startDate" TIMESTAMP(3) NOT NULL, - "endDate" TIMESTAMP(3), - "compatibilityScore" DOUBLE PRECISION, - "lifestyleScore" DOUBLE PRECISION, - "socialScore" DOUBLE PRECISION, - "practicalScore" DOUBLE PRECISION, - "riskScore" DOUBLE PRECISION, - "status" "PlacementStatus" NOT NULL DEFAULT 'ACTIVE', - "endReason" "EndReason", - "satisfactionRating" INTEGER, - "placementNotes" TEXT, - "outcomeNotes" TEXT, - "conflictGap" TEXT, - "wasPredictable" BOOLEAN, - "relatedIncidentId" TEXT, - - CONSTRAINT "Placement_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "CompatibilityAssessment" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "residentId" TEXT NOT NULL, - "comparedWithId" TEXT NOT NULL, - "overallScore" DOUBLE PRECISION NOT NULL, - "lifestyleScore" DOUBLE PRECISION NOT NULL, - "socialScore" DOUBLE PRECISION NOT NULL, - "practicalScore" DOUBLE PRECISION NOT NULL, - "riskScore" DOUBLE PRECISION NOT NULL, - "strengths" TEXT[], - "concerns" TEXT[], - "recommendations" TEXT[], - - CONSTRAINT "CompatibilityAssessment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Incident" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "placementId" TEXT, - "reportedById" TEXT, - "subjectId" TEXT, - "date" TIMESTAMP(3) NOT NULL, - "category" "IncidentCategory" NOT NULL DEFAULT 'INTERPERSONAL', - "type" "IncidentType" NOT NULL, - "severity" "IncidentSeverity" NOT NULL, - "description" TEXT NOT NULL, - "resolution" TEXT, - "resolvedAt" TIMESTAMP(3), - "predictable" BOOLEAN, - "compatibilityGap" TEXT, - "nextFollowUpDate" TIMESTAMP(3), - "followUpPriority" "FollowUpPriority", - - CONSTRAINT "Incident_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "IncidentFollowUp" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "incidentId" TEXT NOT NULL, - "action" TEXT NOT NULL, - "notes" TEXT, - "outcome" TEXT, - "staffName" TEXT, - "scheduledNextDate" TIMESTAMP(3), - - CONSTRAINT "IncidentFollowUp_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "IncidentInvolvement" ( - "id" TEXT NOT NULL, - "incidentId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "role" "InvolvementRole" NOT NULL DEFAULT 'INVOLVED', - - CONSTRAINT "IncidentInvolvement_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "SatisfactionCheckIn" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "placementId" TEXT NOT NULL, - "checkInType" "CheckInType" NOT NULL, - "weekNumber" INTEGER, - "overallSatisfaction" INTEGER NOT NULL, - "roommateRelations" INTEGER, - "facilitySatisfaction" INTEGER, - "safetyFeeling" INTEGER, - "concerns" TEXT, - "improvements" TEXT, - "positives" TEXT, - "collectedBy" TEXT, - "isAnonymous" BOOLEAN NOT NULL DEFAULT false, - - CONSTRAINT "SatisfactionCheckIn_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AlgorithmWeight" ( - "id" TEXT NOT NULL, - "updatedAt" TIMESTAMP(3) NOT NULL, - "lifestyleWeight" DOUBLE PRECISION NOT NULL DEFAULT 30, - "socialWeight" DOUBLE PRECISION NOT NULL DEFAULT 25, - "practicalWeight" DOUBLE PRECISION NOT NULL DEFAULT 25, - "riskWeight" DOUBLE PRECISION NOT NULL DEFAULT 20, - "factorWeights" JSONB NOT NULL, - "version" INTEGER NOT NULL DEFAULT 1, - "active" BOOLEAN NOT NULL DEFAULT true, - "notes" TEXT, - - CONSTRAINT "AlgorithmWeight_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "email" TEXT NOT NULL, - "passwordHash" TEXT, - "name" TEXT NOT NULL, - "role" "StaffRole" NOT NULL DEFAULT 'CASE_WORKER', - "active" BOOLEAN NOT NULL DEFAULT true, - "lastLoginAt" TIMESTAMP(3), - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AuditLog" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "action" TEXT NOT NULL, - "entity" TEXT NOT NULL, - "entityId" TEXT NOT NULL, - "userId" TEXT, - "changes" JSONB, - "reason" TEXT, - - CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "MaintenanceRequest" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "spotId" TEXT, - "category" "MaintenanceCategory" NOT NULL, - "priority" "MaintenancePriority" NOT NULL DEFAULT 'NORMAL', - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "location" TEXT, - "reportedById" TEXT, - "reporterName" TEXT, - "assignedTo" TEXT, - "assignedAt" TIMESTAMP(3), - "status" "MaintenanceStatus" NOT NULL DEFAULT 'OPEN', - "startedAt" TIMESTAMP(3), - "completedAt" TIMESTAMP(3), - "resolution" TEXT, - "cost" DOUBLE PRECISION, - "notes" TEXT, - - CONSTRAINT "MaintenanceRequest_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "HouseholdTask" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT, - "instructions" TEXT, - "taskType" "HouseholdTaskType" NOT NULL DEFAULT 'ONE_TIME', - "category" "HouseholdTaskCategory" NOT NULL DEFAULT 'OTHER', - "priority" "HouseholdTaskPriority" NOT NULL DEFAULT 'NORMAL', - "scheduleHuman" TEXT, - "estimatedMinutes" INTEGER, - "currentStatus" "HouseholdTaskStatus" NOT NULL DEFAULT 'IDLE', - "isCompleted" BOOLEAN NOT NULL DEFAULT false, - "completedAt" TIMESTAMP(3), - "createdByResidentId" TEXT, - "createdByStaff" TEXT, - - CONSTRAINT "HouseholdTask_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskCompletion" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "taskId" TEXT NOT NULL, - "completedById" TEXT NOT NULL, - "completedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "notes" TEXT, - "durationMinutes" INTEGER, - - CONSTRAINT "TaskCompletion_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskAttentionFlag" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "taskId" TEXT NOT NULL, - "flaggedById" TEXT NOT NULL, - "message" TEXT, - "isResolved" BOOLEAN NOT NULL DEFAULT false, - "resolvedAt" TIMESTAMP(3), - "resolvedByCompletionId" TEXT, - - CONSTRAINT "TaskAttentionFlag_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TaskRequest" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "taskId" TEXT NOT NULL, - "requestedById" TEXT NOT NULL, - "requestedResidentId" TEXT, - "isBroadcast" BOOLEAN NOT NULL DEFAULT false, - "message" TEXT, - "status" "TaskRequestStatus" NOT NULL DEFAULT 'PENDING', - "responseMessage" TEXT, - "completionId" TEXT, - - CONSTRAINT "TaskRequest_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Resident_code_key" ON "Resident"("code"); - --- CreateIndex -CREATE INDEX "Resident_status_idx" ON "Resident"("status"); - --- CreateIndex -CREATE INDEX "Resident_ageRange_gender_idx" ON "Resident"("ageRange", "gender"); - --- CreateIndex -CREATE UNIQUE INDEX "HousingUnit_code_key" ON "HousingUnit"("code"); - --- CreateIndex -CREATE INDEX "HousingUnit_status_idx" ON "HousingUnit"("status"); - --- CreateIndex -CREATE INDEX "HousingUnit_totalBeds_idx" ON "HousingUnit"("totalBeds"); - --- CreateIndex -CREATE INDEX "PlacementSpot_type_status_idx" ON "PlacementSpot"("type", "status"); - --- CreateIndex -CREATE INDEX "PlacementSpot_requiresMedicalDocs_idx" ON "PlacementSpot"("requiresMedicalDocs"); - --- CreateIndex -CREATE INDEX "PlacementSpot_housingUnitId_idx" ON "PlacementSpot"("housingUnitId"); - --- CreateIndex -CREATE UNIQUE INDEX "PlacementSpot_housingUnitId_code_key" ON "PlacementSpot"("housingUnitId", "code"); - --- CreateIndex -CREATE INDEX "Placement_status_idx" ON "Placement"("status"); - --- CreateIndex -CREATE INDEX "Placement_startDate_endDate_idx" ON "Placement"("startDate", "endDate"); - --- CreateIndex -CREATE INDEX "Placement_residentId_idx" ON "Placement"("residentId"); - --- CreateIndex -CREATE INDEX "Placement_housingUnitId_idx" ON "Placement"("housingUnitId"); - --- CreateIndex -CREATE UNIQUE INDEX "Placement_residentId_housingUnitId_startDate_key" ON "Placement"("residentId", "housingUnitId", "startDate"); - --- CreateIndex -CREATE INDEX "CompatibilityAssessment_overallScore_idx" ON "CompatibilityAssessment"("overallScore"); - --- CreateIndex -CREATE UNIQUE INDEX "CompatibilityAssessment_residentId_comparedWithId_key" ON "CompatibilityAssessment"("residentId", "comparedWithId"); - --- CreateIndex -CREATE INDEX "Incident_type_severity_idx" ON "Incident"("type", "severity"); - --- CreateIndex -CREATE INDEX "Incident_date_idx" ON "Incident"("date"); - --- CreateIndex -CREATE INDEX "Incident_reportedById_idx" ON "Incident"("reportedById"); - --- CreateIndex -CREATE INDEX "Incident_subjectId_idx" ON "Incident"("subjectId"); - --- CreateIndex -CREATE INDEX "Incident_nextFollowUpDate_idx" ON "Incident"("nextFollowUpDate"); - --- CreateIndex -CREATE INDEX "IncidentFollowUp_incidentId_idx" ON "IncidentFollowUp"("incidentId"); - --- CreateIndex -CREATE INDEX "IncidentFollowUp_createdAt_idx" ON "IncidentFollowUp"("createdAt"); - --- CreateIndex -CREATE INDEX "IncidentInvolvement_residentId_idx" ON "IncidentInvolvement"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "IncidentInvolvement_incidentId_residentId_key" ON "IncidentInvolvement"("incidentId", "residentId"); - --- CreateIndex -CREATE INDEX "SatisfactionCheckIn_placementId_idx" ON "SatisfactionCheckIn"("placementId"); - --- CreateIndex -CREATE INDEX "SatisfactionCheckIn_checkInType_idx" ON "SatisfactionCheckIn"("checkInType"); - --- CreateIndex -CREATE INDEX "AlgorithmWeight_active_idx" ON "AlgorithmWeight"("active"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE INDEX "User_email_idx" ON "User"("email"); - --- CreateIndex -CREATE INDEX "User_role_idx" ON "User"("role"); - --- CreateIndex -CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog"("entity", "entityId"); - --- CreateIndex -CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt"); - --- CreateIndex -CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId"); - --- CreateIndex -CREATE INDEX "MaintenanceRequest_housingUnitId_idx" ON "MaintenanceRequest"("housingUnitId"); - --- CreateIndex -CREATE INDEX "MaintenanceRequest_status_idx" ON "MaintenanceRequest"("status"); - --- CreateIndex -CREATE INDEX "MaintenanceRequest_priority_status_idx" ON "MaintenanceRequest"("priority", "status"); - --- CreateIndex -CREATE INDEX "MaintenanceRequest_createdAt_idx" ON "MaintenanceRequest"("createdAt"); - --- CreateIndex -CREATE INDEX "MaintenanceRequest_reportedById_idx" ON "MaintenanceRequest"("reportedById"); - --- CreateIndex -CREATE INDEX "HouseholdTask_housingUnitId_currentStatus_idx" ON "HouseholdTask"("housingUnitId", "currentStatus"); - --- CreateIndex -CREATE INDEX "HouseholdTask_housingUnitId_category_idx" ON "HouseholdTask"("housingUnitId", "category"); - --- CreateIndex -CREATE INDEX "TaskCompletion_taskId_idx" ON "TaskCompletion"("taskId"); - --- CreateIndex -CREATE INDEX "TaskCompletion_completedById_idx" ON "TaskCompletion"("completedById"); - --- CreateIndex -CREATE INDEX "TaskAttentionFlag_taskId_idx" ON "TaskAttentionFlag"("taskId"); - --- CreateIndex -CREATE INDEX "TaskRequest_taskId_idx" ON "TaskRequest"("taskId"); - --- CreateIndex -CREATE INDEX "TaskRequest_requestedResidentId_idx" ON "TaskRequest"("requestedResidentId"); - --- AddForeignKey -ALTER TABLE "PlacementSpot" ADD CONSTRAINT "PlacementSpot_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "PlacementSpot" ADD CONSTRAINT "PlacementSpot_parentSpotId_fkey" FOREIGN KEY ("parentSpotId") REFERENCES "PlacementSpot"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Placement" ADD CONSTRAINT "Placement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Placement" ADD CONSTRAINT "Placement_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Placement" ADD CONSTRAINT "Placement_spotId_fkey" FOREIGN KEY ("spotId") REFERENCES "PlacementSpot"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Placement" ADD CONSTRAINT "Placement_relatedIncidentId_fkey" FOREIGN KEY ("relatedIncidentId") REFERENCES "Incident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "CompatibilityAssessment" ADD CONSTRAINT "CompatibilityAssessment_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "CompatibilityAssessment" ADD CONSTRAINT "CompatibilityAssessment_comparedWithId_fkey" FOREIGN KEY ("comparedWithId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Incident" ADD CONSTRAINT "Incident_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Incident" ADD CONSTRAINT "Incident_placementId_fkey" FOREIGN KEY ("placementId") REFERENCES "Placement"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Incident" ADD CONSTRAINT "Incident_reportedById_fkey" FOREIGN KEY ("reportedById") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Incident" ADD CONSTRAINT "Incident_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "IncidentFollowUp" ADD CONSTRAINT "IncidentFollowUp_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "Incident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "IncidentInvolvement" ADD CONSTRAINT "IncidentInvolvement_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "Incident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "IncidentInvolvement" ADD CONSTRAINT "IncidentInvolvement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_placementId_fkey" FOREIGN KEY ("placementId") REFERENCES "Placement"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_spotId_fkey" FOREIGN KEY ("spotId") REFERENCES "PlacementSpot"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MaintenanceRequest" ADD CONSTRAINT "MaintenanceRequest_reportedById_fkey" FOREIGN KEY ("reportedById") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseholdTask" ADD CONSTRAINT "HouseholdTask_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseholdTask" ADD CONSTRAINT "HouseholdTask_createdByResidentId_fkey" FOREIGN KEY ("createdByResidentId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskCompletion" ADD CONSTRAINT "TaskCompletion_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "HouseholdTask"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskCompletion" ADD CONSTRAINT "TaskCompletion_completedById_fkey" FOREIGN KEY ("completedById") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "HouseholdTask"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_flaggedById_fkey" FOREIGN KEY ("flaggedById") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskAttentionFlag" ADD CONSTRAINT "TaskAttentionFlag_resolvedByCompletionId_fkey" FOREIGN KEY ("resolvedByCompletionId") REFERENCES "TaskCompletion"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "HouseholdTask"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_requestedResidentId_fkey" FOREIGN KEY ("requestedResidentId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TaskRequest" ADD CONSTRAINT "TaskRequest_completionId_fkey" FOREIGN KEY ("completionId") REFERENCES "TaskCompletion"("id") ON DELETE SET NULL ON UPDATE CASCADE; - diff --git a/prisma/migrations/20260223000000_add_preferences_completed_at/migration.sql b/prisma/migrations/20260223000000_add_preferences_completed_at/migration.sql deleted file mode 100644 index ecd25dfc..00000000 --- a/prisma/migrations/20260223000000_add_preferences_completed_at/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Resident" ADD COLUMN "preferencesCompletedAt" TIMESTAMP(3); diff --git a/prisma/migrations/20260223_add_transfer_requests/migration.sql b/prisma/migrations/20260223_add_transfer_requests/migration.sql deleted file mode 100644 index 4317264b..00000000 --- a/prisma/migrations/20260223_add_transfer_requests/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ --- CreateEnum -CREATE TYPE "TransferRequestStatus" AS ENUM ('PENDING', 'APPROVED', 'DENIED', 'COMPLETED', 'CANCELLED'); - --- CreateTable -CREATE TABLE "TransferRequest" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "currentPlacementId" TEXT, - "targetUnitId" TEXT, - "reason" TEXT NOT NULL, - "status" "TransferRequestStatus" NOT NULL DEFAULT 'PENDING', - "staffNotes" TEXT, - "reviewedBy" TEXT, - "reviewedAt" TIMESTAMP(3), - - CONSTRAINT "TransferRequest_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "TransferRequest_residentId_idx" ON "TransferRequest"("residentId"); -CREATE INDEX "TransferRequest_status_idx" ON "TransferRequest"("status"); - --- AddForeignKey -ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_currentPlacementId_fkey" FOREIGN KEY ("currentPlacementId") REFERENCES "Placement"("id") ON DELETE SET NULL ON UPDATE CASCADE; -ALTER TABLE "TransferRequest" ADD CONSTRAINT "TransferRequest_targetUnitId_fkey" FOREIGN KEY ("targetUnitId") REFERENCES "HousingUnit"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260223_simplify_roles_admin_only/migration.sql b/prisma/migrations/20260223_simplify_roles_admin_only/migration.sql deleted file mode 100644 index 6f8c19c2..00000000 --- a/prisma/migrations/20260223_simplify_roles_admin_only/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Migrate all existing users to ADMIN role -UPDATE "User" SET "role" = 'ADMIN' WHERE "role" != 'ADMIN'; - --- Drop the default before the type change: Postgres cannot auto-cast an --- existing default when the column type changes (error 42804 on fresh replay) -ALTER TABLE "User" ALTER COLUMN "role" DROP DEFAULT; - --- Remove old enum values (PostgreSQL: recreate enum) -ALTER TYPE "StaffRole" RENAME TO "StaffRole_old"; -CREATE TYPE "StaffRole" AS ENUM ('ADMIN'); -ALTER TABLE "User" ALTER COLUMN "role" TYPE "StaffRole" USING "role"::text::"StaffRole"; -DROP TYPE "StaffRole_old"; - --- Restore the default on the new enum type -ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'ADMIN'; diff --git a/prisma/migrations/20260225_unified_code_auth/migration.sql b/prisma/migrations/20260225_unified_code_auth/migration.sql deleted file mode 100644 index a5852cc4..00000000 --- a/prisma/migrations/20260225_unified_code_auth/migration.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Unified code-based authentication --- Replace email/password login with code-based login for staff users. --- Staff codes: AOZ-XXXXXX, Resident codes: RES-XXXXXX (already exist on Resident model). - --- Step 1: Add code column as nullable first -ALTER TABLE "User" ADD COLUMN "code" TEXT; - --- Step 2: Generate codes for existing users (AOZ- prefix + first 6 chars of id) -UPDATE "User" SET "code" = 'AOZ-' || UPPER(SUBSTRING(REPLACE(id, '-', '') FROM 1 FOR 6)) -WHERE "code" IS NULL; - --- Step 3: Make code NOT NULL and add unique constraint + index -ALTER TABLE "User" ALTER COLUMN "code" SET NOT NULL; -ALTER TABLE "User" ADD CONSTRAINT "User_code_key" UNIQUE ("code"); -CREATE INDEX "User_code_idx" ON "User"("code"); - --- Step 4: Make email optional (drop NOT NULL) -ALTER TABLE "User" ALTER COLUMN "email" DROP NOT NULL; - --- Step 5: Drop old email index, keep unique constraint -DROP INDEX IF EXISTS "User_email_idx"; diff --git a/prisma/migrations/20260518093000_add_activities/migration.sql b/prisma/migrations/20260518093000_add_activities/migration.sql deleted file mode 100644 index 5163686a..00000000 --- a/prisma/migrations/20260518093000_add_activities/migration.sql +++ /dev/null @@ -1,47 +0,0 @@ --- CreateEnum -CREATE TYPE "ActivityCategory" AS ENUM ('SPORT', 'LANGUAGE', 'CULTURE', 'COMMUNITY', 'FAMILY', 'SUPPORT'); - --- CreateEnum -CREATE TYPE "ActivityCost" AS ENUM ('FREE', 'REDUCED', 'PAID'); - --- CreateEnum -CREATE TYPE "ActivityStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); - --- CreateTable -CREATE TABLE "Activity" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "category" "ActivityCategory" NOT NULL, - "cost" "ActivityCost" NOT NULL DEFAULT 'FREE', - "costNote" TEXT, - "location" TEXT, - "website" TEXT, - "phone" TEXT, - "schedule" TEXT, - "startsAt" TIMESTAMP(3), - "endsAt" TIMESTAMP(3), - "status" "ActivityStatus" NOT NULL DEFAULT 'DRAFT', - "highlight" BOOLEAN NOT NULL DEFAULT false, - "createdByUserId" TEXT, - "updatedByUserId" TEXT, - - CONSTRAINT "Activity_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "Activity_status_category_idx" ON "Activity"("status", "category"); - --- CreateIndex -CREATE INDEX "Activity_status_highlight_idx" ON "Activity"("status", "highlight"); - --- CreateIndex -CREATE INDEX "Activity_endsAt_idx" ON "Activity"("endsAt"); - --- AddForeignKey -ALTER TABLE "Activity" ADD CONSTRAINT "Activity_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Activity" ADD CONSTRAINT "Activity_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260729000000_add_mediation_minutes_and_system_config/migration.sql b/prisma/migrations/20260729000000_add_mediation_minutes_and_system_config/migration.sql deleted file mode 100644 index a7daec5e..00000000 --- a/prisma/migrations/20260729000000_add_mediation_minutes_and_system_config/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Repair schema drift: these objects exist in schema.prisma (and in databases --- updated via `prisma db push`) but were never captured in a migration, so --- fresh replays (CI/E2E, new envs) were missing them. IF NOT EXISTS makes this --- a no-op on databases that already have them. - --- AlterTable -ALTER TABLE "Incident" ADD COLUMN IF NOT EXISTS "mediationMinutes" INTEGER; - --- CreateTable -CREATE TABLE IF NOT EXISTS "SystemConfig" ( - "id" TEXT NOT NULL DEFAULT 'singleton', - "updatedAt" TIMESTAMP(3) NOT NULL, - "pilotBaselineIncidentsPerMonth" DOUBLE PRECISION, - "pilotBaselineRelocationsPerMonth" DOUBLE PRECISION, - "pilotBaselineMediationHoursPerWeek" DOUBLE PRECISION, - "pilotStartDate" TIMESTAMP(3), - - CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id") -); diff --git a/prisma/migrations/20260806130000_add_house_rules_governance/migration.sql b/prisma/migrations/20260806130000_add_house_rules_governance/migration.sql deleted file mode 100644 index 73c6e54c..00000000 --- a/prisma/migrations/20260806130000_add_house_rules_governance/migration.sql +++ /dev/null @@ -1,240 +0,0 @@ --- CreateEnum -CREATE TYPE "RuleScope" AS ENUM ('ORG', 'UNIT'); - --- CreateEnum -CREATE TYPE "RuleDelegation" AS ENUM ('FIXED', 'UNIT_MAY_STRENGTHEN', 'UNIT_DECIDES'); - --- CreateEnum -CREATE TYPE "RuleStatus" AS ENUM ('ACTIVE', 'SUPERSEDED', 'ARCHIVED'); - --- CreateEnum -CREATE TYPE "RuleCategory" AS ENUM ('SAFETY', 'RESPECT', 'NOISE', 'CLEANLINESS', 'KITCHEN', 'BATHROOM', 'GUESTS', 'SHARED_SPACES', 'COSTS', 'COMMUNICATION', 'OTHER'); - --- CreateEnum -CREATE TYPE "ProposalType" AS ENUM ('ADD_RULE', 'AMEND_RULE', 'REPEAL_RULE', 'HOUSE_DECISION'); - --- CreateEnum -CREATE TYPE "ProposalStatus" AS ENUM ('DISCUSSION', 'VOTING', 'NEEDS_STAFF_CONFIRMATION', 'ACCEPTED', 'REJECTED', 'WITHDRAWN', 'VETOED', 'EXPIRED'); - --- CreateEnum -CREATE TYPE "DecisionMode" AS ENUM ('RESIDENT_BINDING', 'RESIDENT_ADVISORY', 'STAFF_ONLY'); - --- CreateEnum -CREATE TYPE "VoteThreshold" AS ENUM ('CONSENSUS', 'SUPERMAJORITY', 'SIMPLE_MAJORITY'); - --- CreateEnum -CREATE TYPE "VoteChoice" AS ENUM ('YES', 'NO', 'ABSTAIN', 'BLOCK'); - --- CreateEnum -CREATE TYPE "StaffDecision" AS ENUM ('CONFIRMED', 'VETOED'); - --- CreateEnum -CREATE TYPE "ResolutionStage" AS ENUM ('REPORTED', 'SELF_RESOLUTION', 'PEER_MEDIATION', 'STAFF_MEDIATION', 'FORMAL_MEASURE', 'CLOSED'); - --- CreateEnum -CREATE TYPE "AgreementStatus" AS ENUM ('PROPOSED', 'ACCEPTED', 'HELD', 'BROKEN', 'EXPIRED'); - --- AlterTable -ALTER TABLE "Incident" ADD COLUMN "resolutionStage" "ResolutionStage" NOT NULL DEFAULT 'REPORTED', -ADD COLUMN "stageEnteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; - --- CreateTable -CREATE TABLE "HouseRule" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "scope" "RuleScope" NOT NULL, - "housingUnitId" TEXT, - "key" TEXT, - "category" "RuleCategory" NOT NULL, - "title" TEXT NOT NULL, - "body" TEXT NOT NULL, - "delegation" "RuleDelegation" NOT NULL DEFAULT 'FIXED', - "parentRuleId" TEXT, - "status" "RuleStatus" NOT NULL DEFAULT 'ACTIVE', - "version" INTEGER NOT NULL DEFAULT 1, - "effectiveFrom" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "effectiveUntil" TIMESTAMP(3), - "adoptedByProposalId" TEXT, - "createdByStaff" TEXT, - - CONSTRAINT "HouseRule_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "RuleAcknowledgement" ( - "id" TEXT NOT NULL, - "ruleId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "ruleVersion" INTEGER NOT NULL, - "acknowledgedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "RuleAcknowledgement_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Proposal" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "type" "ProposalType" NOT NULL, - "category" "RuleCategory" NOT NULL, - "title" TEXT NOT NULL, - "body" TEXT NOT NULL, - "targetRuleId" TEXT, - "parentOrgRuleId" TEXT, - "proposedByResidentId" TEXT, - "proposedByStaff" TEXT, - "status" "ProposalStatus" NOT NULL DEFAULT 'DISCUSSION', - "decisionMode" "DecisionMode" NOT NULL, - "threshold" "VoteThreshold" NOT NULL, - "quorumPercent" INTEGER NOT NULL, - "approvalPercent" INTEGER NOT NULL, - "eligibleVoterCount" INTEGER NOT NULL DEFAULT 0, - "discussionEndsAt" TIMESTAMP(3), - "votingOpenedAt" TIMESTAMP(3), - "votingEndsAt" TIMESTAMP(3), - "decidedAt" TIMESTAMP(3), - "outcomeSummary" TEXT, - "staffDecision" "StaffDecision", - "staffNotes" TEXT, - "staffUserId" TEXT, - "staffDecidedAt" TIMESTAMP(3), - - CONSTRAINT "Proposal_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Vote" ( - "id" TEXT NOT NULL, - "proposalId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "choice" "VoteChoice" NOT NULL, - "reason" TEXT, - "castAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Vote_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ConflictAgreement" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "incidentId" TEXT NOT NULL, - "terms" TEXT NOT NULL, - "mediatorName" TEXT, - "reviewDate" TIMESTAMP(3) NOT NULL, - "status" "AgreementStatus" NOT NULL DEFAULT 'PROPOSED', - "outcomeNotes" TEXT, - "reviewedAt" TIMESTAMP(3), - "ruleProposalId" TEXT, - - CONSTRAINT "ConflictAgreement_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "AgreementParty" ( - "id" TEXT NOT NULL, - "agreementId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "acceptedAt" TIMESTAMP(3), - "declinedAt" TIMESTAMP(3), - - CONSTRAINT "AgreementParty_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "HouseRule_key_key" ON "HouseRule"("key"); - --- CreateIndex -CREATE INDEX "HouseRule_scope_status_idx" ON "HouseRule"("scope", "status"); - --- CreateIndex -CREATE INDEX "HouseRule_housingUnitId_status_idx" ON "HouseRule"("housingUnitId", "status"); - --- CreateIndex -CREATE INDEX "HouseRule_category_idx" ON "HouseRule"("category"); - --- CreateIndex -CREATE INDEX "HouseRule_parentRuleId_idx" ON "HouseRule"("parentRuleId"); - --- CreateIndex -CREATE INDEX "RuleAcknowledgement_residentId_idx" ON "RuleAcknowledgement"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "RuleAcknowledgement_ruleId_residentId_ruleVersion_key" ON "RuleAcknowledgement"("ruleId", "residentId", "ruleVersion"); - --- CreateIndex -CREATE INDEX "Proposal_housingUnitId_status_idx" ON "Proposal"("housingUnitId", "status"); - --- CreateIndex -CREATE INDEX "Proposal_status_votingEndsAt_idx" ON "Proposal"("status", "votingEndsAt"); - --- CreateIndex -CREATE INDEX "Vote_residentId_idx" ON "Vote"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "Vote_proposalId_residentId_key" ON "Vote"("proposalId", "residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "ConflictAgreement_ruleProposalId_key" ON "ConflictAgreement"("ruleProposalId"); - --- CreateIndex -CREATE INDEX "ConflictAgreement_incidentId_idx" ON "ConflictAgreement"("incidentId"); - --- CreateIndex -CREATE INDEX "ConflictAgreement_status_reviewDate_idx" ON "ConflictAgreement"("status", "reviewDate"); - --- CreateIndex -CREATE INDEX "AgreementParty_residentId_idx" ON "AgreementParty"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "AgreementParty_agreementId_residentId_key" ON "AgreementParty"("agreementId", "residentId"); - --- AddForeignKey -ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_parentRuleId_fkey" FOREIGN KEY ("parentRuleId") REFERENCES "HouseRule"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseRule" ADD CONSTRAINT "HouseRule_adoptedByProposalId_fkey" FOREIGN KEY ("adoptedByProposalId") REFERENCES "Proposal"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "RuleAcknowledgement" ADD CONSTRAINT "RuleAcknowledgement_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "HouseRule"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "RuleAcknowledgement" ADD CONSTRAINT "RuleAcknowledgement_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_targetRuleId_fkey" FOREIGN KEY ("targetRuleId") REFERENCES "HouseRule"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_parentOrgRuleId_fkey" FOREIGN KEY ("parentOrgRuleId") REFERENCES "HouseRule"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Proposal" ADD CONSTRAINT "Proposal_proposedByResidentId_fkey" FOREIGN KEY ("proposedByResidentId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Vote" ADD CONSTRAINT "Vote_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Vote" ADD CONSTRAINT "Vote_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ConflictAgreement" ADD CONSTRAINT "ConflictAgreement_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "Incident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ConflictAgreement" ADD CONSTRAINT "ConflictAgreement_ruleProposalId_fkey" FOREIGN KEY ("ruleProposalId") REFERENCES "Proposal"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AgreementParty" ADD CONSTRAINT "AgreementParty_agreementId_fkey" FOREIGN KEY ("agreementId") REFERENCES "ConflictAgreement"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AgreementParty" ADD CONSTRAINT "AgreementParty_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - diff --git a/prisma/migrations/20260806140000_split_cleanliness_dimensions/migration.sql b/prisma/migrations/20260806140000_split_cleanliness_dimensions/migration.sql deleted file mode 100644 index 303ffce4..00000000 --- a/prisma/migrations/20260806140000_split_cleanliness_dimensions/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Split the single "cleanlinessLevel" scale into the three distinct things it --- was conflating. Conflicts about cleanliness are not caused by a difference in --- tidiness as such — they are caused by the directional gap between what one --- person expects of others and what another person actually does, softened by --- how much disorder the first person can live with. - --- The old scale was filled in as "how tidy is this person", so it carries over --- to practice without data loss. RENAME rather than DROP/ADD: dropping would --- discard every resident's answer. -ALTER TABLE "Resident" RENAME COLUMN "cleanlinessLevel" TO "cleanlinessPractice"; - --- The two new dimensions start neutral rather than derived from practice. --- Deriving them would be fabricating answers residents never gave: a tidy --- person does not necessarily demand tidiness from others, and tolerance for --- mess is genuinely independent of one's own habits. The portal asks for them. -ALTER TABLE "Resident" - ADD COLUMN "cleanlinessExpectation" INTEGER NOT NULL DEFAULT 3, - ADD COLUMN "chaosTolerance" INTEGER NOT NULL DEFAULT 3; diff --git a/prisma/migrations/20260813000000_add_expenses_and_resident_profiles/migration.sql b/prisma/migrations/20260813000000_add_expenses_and_resident_profiles/migration.sql deleted file mode 100644 index c19382b5..00000000 --- a/prisma/migrations/20260813000000_add_expenses_and_resident_profiles/migration.sql +++ /dev/null @@ -1,95 +0,0 @@ --- AlterTable -ALTER TABLE "HousingUnit" ADD COLUMN "nickname" TEXT; - --- AlterTable -ALTER TABLE "Resident" ADD COLUMN "bio" TEXT, -ADD COLUMN "displayName" TEXT; - --- CreateTable -CREATE TABLE "ResidentPhoto" ( - "residentId" TEXT NOT NULL, - "data" BYTEA NOT NULL, - "mimeType" TEXT NOT NULL, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ResidentPhoto_pkey" PRIMARY KEY ("residentId") -); - --- CreateTable -CREATE TABLE "Expense" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "paidById" TEXT NOT NULL, - "createdById" TEXT NOT NULL, - "description" TEXT NOT NULL, - "category" TEXT NOT NULL, - "amountRappen" INTEGER NOT NULL, - "date" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Expense_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ExpenseShare" ( - "id" TEXT NOT NULL, - "expenseId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "amountRappen" INTEGER NOT NULL, - - CONSTRAINT "ExpenseShare_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Settlement" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "housingUnitId" TEXT NOT NULL, - "fromId" TEXT NOT NULL, - "toId" TEXT NOT NULL, - "amountRappen" INTEGER NOT NULL, - "note" TEXT, - - CONSTRAINT "Settlement_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "Expense_housingUnitId_date_idx" ON "Expense"("housingUnitId", "date"); - --- CreateIndex -CREATE INDEX "ExpenseShare_residentId_idx" ON "ExpenseShare"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "ExpenseShare_expenseId_residentId_key" ON "ExpenseShare"("expenseId", "residentId"); - --- CreateIndex -CREATE INDEX "Settlement_housingUnitId_idx" ON "Settlement"("housingUnitId"); - --- AddForeignKey -ALTER TABLE "ResidentPhoto" ADD CONSTRAINT "ResidentPhoto_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Expense" ADD CONSTRAINT "Expense_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Expense" ADD CONSTRAINT "Expense_paidById_fkey" FOREIGN KEY ("paidById") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Expense" ADD CONSTRAINT "Expense_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ExpenseShare" ADD CONSTRAINT "ExpenseShare_expenseId_fkey" FOREIGN KEY ("expenseId") REFERENCES "Expense"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ExpenseShare" ADD CONSTRAINT "ExpenseShare_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Settlement" ADD CONSTRAINT "Settlement_toId_fkey" FOREIGN KEY ("toId") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - diff --git a/prisma/migrations/20260813210000_modern_auth_email_password/migration.sql b/prisma/migrations/20260813210000_modern_auth_email_password/migration.sql deleted file mode 100644 index 0b3726ee..00000000 --- a/prisma/migrations/20260813210000_modern_auth_email_password/migration.sql +++ /dev/null @@ -1,43 +0,0 @@ --- CreateEnum -CREATE TYPE "AuthTokenPurpose" AS ENUM ('VERIFY_EMAIL', 'RESET_PASSWORD'); - --- AlterTable -ALTER TABLE "Resident" ADD COLUMN "email" TEXT, -ADD COLUMN "emailVerifiedAt" TIMESTAMP(3), -ADD COLUMN "passwordHash" TEXT; - --- AlterTable -ALTER TABLE "User" ADD COLUMN "emailVerifiedAt" TIMESTAMP(3); - --- CreateTable -CREATE TABLE "AuthToken" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "tokenHash" TEXT NOT NULL, - "purpose" "AuthTokenPurpose" NOT NULL, - "expiresAt" TIMESTAMP(3) NOT NULL, - "usedAt" TIMESTAMP(3), - "userId" TEXT, - "residentId" TEXT, - - CONSTRAINT "AuthToken_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "AuthToken_tokenHash_key" ON "AuthToken"("tokenHash"); - --- CreateIndex -CREATE INDEX "AuthToken_userId_purpose_idx" ON "AuthToken"("userId", "purpose"); - --- CreateIndex -CREATE INDEX "AuthToken_residentId_purpose_idx" ON "AuthToken"("residentId", "purpose"); - --- CreateIndex -CREATE UNIQUE INDEX "Resident_email_key" ON "Resident"("email"); - --- AddForeignKey -ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - diff --git a/prisma/migrations/20260814000000_accounts_link_identities/migration.sql b/prisma/migrations/20260814000000_accounts_link_identities/migration.sql deleted file mode 100644 index 07a295c2..00000000 --- a/prisma/migrations/20260814000000_accounts_link_identities/migration.sql +++ /dev/null @@ -1,98 +0,0 @@ --- Credentials move OFF the identity rows (User, Resident) onto Account, so one --- human can hold both roles with ONE login. Order matters: create the table, --- carry existing emails across, and only then drop the old columns. - --- CreateTable -CREATE TABLE "Account" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "email" TEXT NOT NULL, - "passwordHash" TEXT, - "emailVerifiedAt" TIMESTAMP(3), - "userId" TEXT, - "residentId" TEXT, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Account_email_key" ON "Account"("email"); -CREATE UNIQUE INDEX "Account_userId_key" ON "Account"("userId"); -CREATE UNIQUE INDEX "Account_residentId_key" ON "Account"("residentId"); -CREATE INDEX "Account_userId_idx" ON "Account"("userId"); -CREATE INDEX "Account_residentId_idx" ON "Account"("residentId"); - --- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE "Account" ADD CONSTRAINT "Account_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- Carry known emails across — and the passwords worth carrying. --- --- Cost-10 hashes ($2b$10$) are leftovers from the retired email/password login --- (Feb 2026): nobody knows those passwords, and keeping one would report the --- code as "already registered" and lock its owner out forever. Today's hashes --- are cost 12 (lib/auth/passwords.ts), so the work factor is an exact, honest --- discriminator between a dead credential and a live one. A dropped hash leaves --- the account in its true state — known email, no password — recoverable via --- /forgot-password, which is precisely the mailbox-control proof it should need. -INSERT INTO "Account" ("id", "createdAt", "updatedAt", "email", "passwordHash", "emailVerifiedAt", "userId") -SELECT - md5(random()::text || clock_timestamp()::text || "id"), - now(), now(), - lower("email"), - CASE WHEN "passwordHash" LIKE '$2%$10$%' THEN NULL ELSE "passwordHash" END, - "emailVerifiedAt", - "id" -FROM "User" -WHERE "email" IS NOT NULL AND "email" <> ''; - -INSERT INTO "Account" ("id", "createdAt", "updatedAt", "email", "passwordHash", "emailVerifiedAt", "residentId") -SELECT - md5(random()::text || clock_timestamp()::text || "id"), - now(), now(), - lower("email"), - CASE WHEN "passwordHash" LIKE '$2%$10$%' THEN NULL ELSE "passwordHash" END, - "emailVerifiedAt", - "id" -FROM "Resident" -WHERE "email" IS NOT NULL AND "email" <> '' - -- One email = one account. Where a staff row already claimed it, the resident - -- code is linked afterwards through /register, which requires the account - -- password — an automatic merge here would join two identities on a string. - AND lower("email") NOT IN (SELECT "email" FROM "Account"); - --- Reset tokens are single-use and short-lived; re-pointing them across a --- credential-model change is not worth the risk of mis-linking one. -DELETE FROM "AuthToken"; - --- DropForeignKey -ALTER TABLE "AuthToken" DROP CONSTRAINT "AuthToken_residentId_fkey"; -ALTER TABLE "AuthToken" DROP CONSTRAINT "AuthToken_userId_fkey"; - --- DropIndex -DROP INDEX "AuthToken_residentId_purpose_idx"; -DROP INDEX "AuthToken_userId_purpose_idx"; -DROP INDEX "Resident_email_key"; -DROP INDEX "User_email_key"; - --- AlterTable -ALTER TABLE "AuthToken" DROP COLUMN "residentId", -DROP COLUMN "userId", -ADD COLUMN "accountId" TEXT NOT NULL; - --- AlterTable -ALTER TABLE "Resident" DROP COLUMN "email", -DROP COLUMN "emailVerifiedAt", -DROP COLUMN "passwordHash"; - --- AlterTable -ALTER TABLE "User" DROP COLUMN "email", -DROP COLUMN "emailVerifiedAt", -DROP COLUMN "passwordHash"; - --- CreateIndex -CREATE INDEX "AuthToken_accountId_purpose_idx" ON "AuthToken"("accountId", "purpose"); - --- AddForeignKey -ALTER TABLE "AuthToken" ADD CONSTRAINT "AuthToken_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260815000000_chore_checklist_rotation_balance/migration.sql b/prisma/migrations/20260815000000_chore_checklist_rotation_balance/migration.sql deleted file mode 100644 index 20881ab3..00000000 --- a/prisma/migrations/20260815000000_chore_checklist_rotation_balance/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Chore fairness: a checkable definition of done, a derived rotation, and the --- record needed to weight contributions by time rather than by row count. --- --- All three columns are additive with defaults, so this migration is safe under --- fleetcrown's apply-schema.sh guard (no DROP / TRUNCATE / ALTER COLUMN TYPE) --- and cannot lose a single existing completion. - --- Definition of done: ordered binary actions, not an outcome description. -ALTER TABLE "HouseholdTask" ADD COLUMN "checklist" TEXT[] DEFAULT ARRAY[]::TEXT[]; - --- Ordered rotation for scheduled tasks. Whose turn it is stays DERIVED. -ALTER TABLE "HouseholdTask" ADD COLUMN "rotationResidentIds" TEXT[] DEFAULT ARRAY[]::TEXT[]; - --- Which checklist items a completion actually ticked, stored by label so that --- amending a task's checklist later cannot rewrite what a past completion said. -ALTER TABLE "TaskCompletion" ADD COLUMN "completedItems" TEXT[] DEFAULT ARRAY[]::TEXT[]; diff --git a/prisma/migrations/20260816100000_roles_learning_building/migration.sql b/prisma/migrations/20260816100000_roles_learning_building/migration.sql deleted file mode 100644 index 6cd9ea8b..00000000 --- a/prisma/migrations/20260816100000_roles_learning_building/migration.sql +++ /dev/null @@ -1,54 +0,0 @@ --- Staff roles beyond ADMIN (Betreuung, Sozialarbeit, Jobcoach), --- optional buildingCode on housing units, and learning records --- (language tests, courses, informal learning). - --- PostgreSQL: ADD VALUE cannot run in a transaction in older versions; --- Prisma wraps migrations in a transaction. Using ADD VALUE IF NOT EXISTS --- (PG 9.1+ for ADD VALUE, IF NOT EXISTS since PG 9.1 isn't there — IF NOT --- EXISTS for enum values is PG 9.1... actually IF NOT EXISTS for enum was --- added in PostgreSQL 9.1? No: ALTER TYPE ... ADD VALUE IF NOT EXISTS is PG 9.1 --- wait, it's PostgreSQL 9.1 no - it's 9.1+ ADD VALUE, IF NOT EXISTS in PG 9.1 --- Correction: IF NOT EXISTS for enum values arrived in PostgreSQL 9.1? --- It's PostgreSQL 9.1 for ADD VALUE, and IF NOT EXISTS in PostgreSQL 9.1... --- PostgreSQL 9.1 added ADD VALUE. IF NOT EXISTS: PostgreSQL 9.1 no, 10? --- Actually: ALTER TYPE ... ADD VALUE IF NOT EXISTS is PostgreSQL 9.1... --- Documented as PostgreSQL 9.1? I'll use IF NOT EXISTS (supported on PG 16). - -ALTER TYPE "StaffRole" ADD VALUE IF NOT EXISTS 'BETREUUNG'; -ALTER TYPE "StaffRole" ADD VALUE IF NOT EXISTS 'SOZIALARBEIT'; -ALTER TYPE "StaffRole" ADD VALUE IF NOT EXISTS 'JOBCOACH'; - -ALTER TABLE "HousingUnit" ADD COLUMN IF NOT EXISTS "buildingCode" TEXT; -CREATE INDEX IF NOT EXISTS "HousingUnit_buildingCode_idx" ON "HousingUnit"("buildingCode"); - -CREATE TYPE "LearningKind" AS ENUM ('LANGUAGE_TEST', 'COURSE', 'INFORMAL', 'QUALIFICATION'); -CREATE TYPE "LearningStatus" AS ENUM ('PLANNED', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED'); -CREATE TYPE "ResidentOrStaff" AS ENUM ('RESIDENT', 'STAFF'); - -CREATE TABLE "LearningRecord" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "kind" "LearningKind" NOT NULL, - "title" TEXT NOT NULL, - "status" "LearningStatus" NOT NULL DEFAULT 'PLANNED', - "languageCode" TEXT, - "cefrLevel" TEXT, - "provider" TEXT, - "category" TEXT, - "hours" INTEGER, - "startedAt" TIMESTAMP(3), - "completedAt" TIMESTAMP(3), - "notes" TEXT, - "recordedBy" "ResidentOrStaff" NOT NULL, - - CONSTRAINT "LearningRecord_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "LearningRecord_residentId_kind_idx" ON "LearningRecord"("residentId", "kind"); -CREATE INDEX "LearningRecord_status_idx" ON "LearningRecord"("status"); -CREATE INDEX "LearningRecord_languageCode_cefrLevel_idx" ON "LearningRecord"("languageCode", "cefrLevel"); - -ALTER TABLE "LearningRecord" ADD CONSTRAINT "LearningRecord_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260816120000_resident_profile_visibility/migration.sql b/prisma/migrations/20260816120000_resident_profile_visibility/migration.sql deleted file mode 100644 index 12992e45..00000000 --- a/prisma/migrations/20260816120000_resident_profile_visibility/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Who may see a resident's self-chosen profile. --- --- The default is ROOMMATES, which is exactly what the photo endpoint already --- enforced in code: self plus the people you currently live with. Choosing it --- as the default means nobody's profile becomes MORE visible the moment this --- migration runs — a privacy setting whose rollout widens existing exposure --- would be the opposite of what it is for. -CREATE TYPE "ProfileVisibility" AS ENUM ('PRIVATE', 'ROOMMATES', 'RESIDENTS'); - -ALTER TABLE "Resident" - ADD COLUMN "profileVisibility" "ProfileVisibility" NOT NULL DEFAULT 'ROOMMATES'; diff --git a/prisma/migrations/20260816140000_resident_staff_messaging/migration.sql b/prisma/migrations/20260816140000_resident_staff_messaging/migration.sql deleted file mode 100644 index c3a4a4e6..00000000 --- a/prisma/migrations/20260816140000_resident_staff_messaging/migration.sql +++ /dev/null @@ -1,54 +0,0 @@ --- One conversation per resident with the staff team. --- --- `residentId` is UNIQUE: a resident has exactly one thread, created on first --- use. Threads-per-topic would ask the person with a question to decide which --- of their concerns is "the same conversation" as another, which is filing work --- we would be handing to them. -CREATE TABLE "MessageThread" ( - "id" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "MessageThread_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "MessageThread_residentId_key" ON "MessageThread"("residentId"); --- A staff inbox sorts by "waiting longest", so this index is what keeps that --- query from reading every thread's messages. -CREATE INDEX "MessageThread_updatedAt_idx" ON "MessageThread"("updatedAt"); - -CREATE TABLE "Message" ( - "id" TEXT NOT NULL, - "threadId" TEXT NOT NULL, - "authorResidentId" TEXT, - "authorUserId" TEXT, - "body" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "readAt" TIMESTAMP(3), - - CONSTRAINT "Message_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "Message_threadId_createdAt_idx" ON "Message"("threadId", "createdAt"); - --- Exactly one author. A message with neither is unattributable and a message --- with both is a lie about who said it; the database refuses both rather than --- trusting every future caller to remember. -ALTER TABLE "Message" ADD CONSTRAINT "Message_one_author" - CHECK (("authorResidentId" IS NOT NULL) <> ("authorUserId" IS NOT NULL)); - --- Deleting a resident removes their thread, but NOT the authorship of what --- anyone said: Restrict on the author columns means a person cannot be erased --- out from under a conversation staff may have to account for later. -ALTER TABLE "MessageThread" ADD CONSTRAINT "MessageThread_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "Message" ADD CONSTRAINT "Message_threadId_fkey" - FOREIGN KEY ("threadId") REFERENCES "MessageThread"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "Message" ADD CONSTRAINT "Message_authorResidentId_fkey" - FOREIGN KEY ("authorResidentId") REFERENCES "Resident"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - -ALTER TABLE "Message" ADD CONSTRAINT "Message_authorUserId_fkey" - FOREIGN KEY ("authorUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260817100000_care_team_learning_kinds/migration.sql b/prisma/migrations/20260817100000_care_team_learning_kinds/migration.sql deleted file mode 100644 index b9981e6e..00000000 --- a/prisma/migrations/20260817100000_care_team_learning_kinds/migration.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Care team (Wohnen / Sozialarbeit / Jobcoach) and two extra learning kinds --- for volunteering hours and community service. - -ALTER TYPE "LearningKind" ADD VALUE IF NOT EXISTS 'VOLUNTEERING'; -ALTER TYPE "LearningKind" ADD VALUE IF NOT EXISTS 'COMMUNITY_SERVICE'; - -CREATE TYPE "CareRole" AS ENUM ('HOUSING', 'SOCIAL', 'JOB'); - -CREATE TABLE "CareAssignment" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "staffId" TEXT NOT NULL, - "role" "CareRole" NOT NULL, - - CONSTRAINT "CareAssignment_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "CareAssignment_residentId_role_key" ON "CareAssignment"("residentId", "role"); -CREATE INDEX "CareAssignment_staffId_idx" ON "CareAssignment"("staffId"); - -ALTER TABLE "CareAssignment" ADD CONSTRAINT "CareAssignment_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "CareAssignment" ADD CONSTRAINT "CareAssignment_staffId_fkey" - FOREIGN KEY ("staffId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260817120000_care_appointments_attributes/migration.sql b/prisma/migrations/20260817120000_care_appointments_attributes/migration.sql deleted file mode 100644 index 0d8887e7..00000000 --- a/prisma/migrations/20260817120000_care_appointments_attributes/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ --- Appointments and catalog-driven care attributes (Wohnen / Sozialarbeit / Jobcoach). - -CREATE TYPE "AppointmentStatus" AS ENUM ('SCHEDULED', 'COMPLETED', 'CANCELLED', 'NO_SHOW'); - -CREATE TABLE "Appointment" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "staffId" TEXT NOT NULL, - "domain" "CareRole" NOT NULL, - "title" TEXT NOT NULL, - "startsAt" TIMESTAMP(3) NOT NULL, - "endsAt" TIMESTAMP(3), - "location" TEXT, - "notes" TEXT, - "status" "AppointmentStatus" NOT NULL DEFAULT 'SCHEDULED', - - CONSTRAINT "Appointment_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "Appointment_residentId_startsAt_idx" ON "Appointment"("residentId", "startsAt"); -CREATE INDEX "Appointment_staffId_startsAt_idx" ON "Appointment"("staffId", "startsAt"); -CREATE INDEX "Appointment_status_idx" ON "Appointment"("status"); - -ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_staffId_fkey" - FOREIGN KEY ("staffId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - -CREATE TABLE "CareAttribute" ( - "id" TEXT NOT NULL, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "domain" "CareRole" NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL, - "updatedById" TEXT NOT NULL, - - CONSTRAINT "CareAttribute_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "CareAttribute_residentId_domain_key_key" ON "CareAttribute"("residentId", "domain", "key"); -CREATE INDEX "CareAttribute_residentId_domain_idx" ON "CareAttribute"("residentId", "domain"); - -ALTER TABLE "CareAttribute" ADD CONSTRAINT "CareAttribute_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "CareAttribute" ADD CONSTRAINT "CareAttribute_updatedById_fkey" - FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260818091141_add_volunteering_role_marketplace_events/migration.sql b/prisma/migrations/20260818091141_add_volunteering_role_marketplace_events/migration.sql deleted file mode 100644 index 326b1035..00000000 --- a/prisma/migrations/20260818091141_add_volunteering_role_marketplace_events/migration.sql +++ /dev/null @@ -1,115 +0,0 @@ --- CreateEnum -CREATE TYPE "HouseEventCategory" AS ENUM ('HOUSE_MEETING', 'SOCIAL', 'CULTURE', 'SUPPORT'); - --- CreateEnum -CREATE TYPE "HouseEventStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'CANCELLED'); - --- CreateEnum -CREATE TYPE "EventRsvpStatus" AS ENUM ('GOING', 'MAYBE', 'DECLINED'); - --- CreateEnum -CREATE TYPE "MarketplacePostKind" AS ENUM ('GIVE_AWAY', 'LEND', 'WANTED'); - --- CreateEnum -CREATE TYPE "MarketplacePostCategory" AS ENUM ('FURNITURE', 'KITCHEN', 'CLOTHING', 'ELECTRONICS', 'KIDS', 'OTHER'); - --- CreateEnum -CREATE TYPE "MarketplacePostStatus" AS ENUM ('OPEN', 'CLAIMED', 'CLOSED'); - --- AlterEnum -ALTER TYPE "CareRole" ADD VALUE 'VOLUNTEERING'; - --- AlterEnum -ALTER TYPE "StaffRole" ADD VALUE 'FREIWILLIGENARBEIT'; - --- CreateTable -CREATE TABLE "HouseEvent" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "category" "HouseEventCategory" NOT NULL DEFAULT 'SOCIAL', - "location" TEXT, - "startsAt" TIMESTAMP(3) NOT NULL, - "endsAt" TIMESTAMP(3), - "status" "HouseEventStatus" NOT NULL DEFAULT 'PUBLISHED', - "createdByStaffId" TEXT, - "createdByResidentId" TEXT, - - CONSTRAINT "HouseEvent_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "EventRsvp" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "eventId" TEXT NOT NULL, - "residentId" TEXT NOT NULL, - "status" "EventRsvpStatus" NOT NULL DEFAULT 'GOING', - - CONSTRAINT "EventRsvp_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "MarketplacePost" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "housingUnitId" TEXT NOT NULL, - "postedById" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "kind" "MarketplacePostKind" NOT NULL, - "category" "MarketplacePostCategory" NOT NULL DEFAULT 'OTHER', - "status" "MarketplacePostStatus" NOT NULL DEFAULT 'OPEN', - "claimedById" TEXT, - "closedAt" TIMESTAMP(3), - "hiddenByStaff" BOOLEAN NOT NULL DEFAULT false, - "hiddenReason" TEXT, - - CONSTRAINT "MarketplacePost_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "HouseEvent_housingUnitId_startsAt_idx" ON "HouseEvent"("housingUnitId", "startsAt"); - --- CreateIndex -CREATE INDEX "HouseEvent_status_startsAt_idx" ON "HouseEvent"("status", "startsAt"); - --- CreateIndex -CREATE INDEX "EventRsvp_eventId_idx" ON "EventRsvp"("eventId"); - --- CreateIndex -CREATE UNIQUE INDEX "EventRsvp_eventId_residentId_key" ON "EventRsvp"("eventId", "residentId"); - --- CreateIndex -CREATE INDEX "MarketplacePost_housingUnitId_status_idx" ON "MarketplacePost"("housingUnitId", "status"); - --- CreateIndex -CREATE INDEX "MarketplacePost_postedById_idx" ON "MarketplacePost"("postedById"); - --- AddForeignKey -ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_createdByStaffId_fkey" FOREIGN KEY ("createdByStaffId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "HouseEvent" ADD CONSTRAINT "HouseEvent_createdByResidentId_fkey" FOREIGN KEY ("createdByResidentId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventRsvp" ADD CONSTRAINT "EventRsvp_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "HouseEvent"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "EventRsvp" ADD CONSTRAINT "EventRsvp_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "HousingUnit"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "MarketplacePost" ADD CONSTRAINT "MarketplacePost_claimedById_fkey" FOREIGN KEY ("claimedById") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260824225746_opportunities_and_applications/migration.sql b/prisma/migrations/20260824225746_opportunities_and_applications/migration.sql deleted file mode 100644 index f029cf93..00000000 --- a/prisma/migrations/20260824225746_opportunities_and_applications/migration.sql +++ /dev/null @@ -1,96 +0,0 @@ --- CreateEnum -CREATE TYPE "OpportunityKind" AS ENUM ('VOLUNTEERING', 'COMMUNITY_SERVICE'); - --- CreateEnum -CREATE TYPE "PermitRequirement" AS ENUM ('NONE', 'EMPLOYER_NOTIFIES', 'PERMIT_REQUIRED'); - --- CreateEnum -CREATE TYPE "OpportunityStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); - --- CreateEnum -CREATE TYPE "ApplicationStage" AS ENUM ('INTERESTED', 'APPLIED', 'INTERVIEW', 'ACCEPTED', 'STARTED', 'ENDED', 'DECLINED'); - --- CreateTable -CREATE TABLE "Opportunity" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "kind" "OpportunityKind" NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "organisation" TEXT NOT NULL, - "location" TEXT, - "schedule" TEXT, - "hoursPerWeek" INTEGER, - "seats" INTEGER, - "germanLevel" TEXT, - "permitRequirement" "PermitRequirement" NOT NULL DEFAULT 'NONE', - "requirementNote" TEXT, - "contactName" TEXT, - "contactEmail" TEXT, - "contactPhone" TEXT, - "website" TEXT, - "status" "OpportunityStatus" NOT NULL DEFAULT 'DRAFT', - "startsAt" TIMESTAMP(3), - "endsAt" TIMESTAMP(3), - "createdByUserId" TEXT, - "updatedByUserId" TEXT, - - CONSTRAINT "Opportunity_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "OpportunityApplication" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "opportunityId" TEXT NOT NULL, - "stage" "ApplicationStage" NOT NULL DEFAULT 'INTERESTED', - "stageChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "note" TEXT, - "createdBy" "ResidentOrStaff" NOT NULL, - "supportedByUserId" TEXT, - "learningRecordId" TEXT, - - CONSTRAINT "OpportunityApplication_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "Opportunity_status_kind_idx" ON "Opportunity"("status", "kind"); - --- CreateIndex -CREATE INDEX "Opportunity_endsAt_idx" ON "Opportunity"("endsAt"); - --- CreateIndex -CREATE UNIQUE INDEX "OpportunityApplication_learningRecordId_key" ON "OpportunityApplication"("learningRecordId"); - --- CreateIndex -CREATE INDEX "OpportunityApplication_stage_idx" ON "OpportunityApplication"("stage"); - --- CreateIndex -CREATE INDEX "OpportunityApplication_opportunityId_stage_idx" ON "OpportunityApplication"("opportunityId", "stage"); - --- CreateIndex -CREATE INDEX "OpportunityApplication_residentId_idx" ON "OpportunityApplication"("residentId"); - --- CreateIndex -CREATE UNIQUE INDEX "OpportunityApplication_residentId_opportunityId_key" ON "OpportunityApplication"("residentId", "opportunityId"); - --- AddForeignKey -ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Opportunity" ADD CONSTRAINT "Opportunity_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_residentId_fkey" FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_opportunityId_fkey" FOREIGN KEY ("opportunityId") REFERENCES "Opportunity"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_supportedByUserId_fkey" FOREIGN KEY ("supportedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "OpportunityApplication" ADD CONSTRAINT "OpportunityApplication_learningRecordId_fkey" FOREIGN KEY ("learningRecordId") REFERENCES "LearningRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260825120000_marketplace_goods_and_services/migration.sql b/prisma/migrations/20260825120000_marketplace_goods_and_services/migration.sql deleted file mode 100644 index f9b1c571..00000000 --- a/prisma/migrations/20260825120000_marketplace_goods_and_services/migration.sql +++ /dev/null @@ -1,36 +0,0 @@ --- Marketplace: goods AND services. --- --- Two kinds are added for exchanges of TIME rather than objects, the category --- column stops being a database enum (vocabulary belongs in config, so a new --- category is one line rather than a migration), and two columns close the --- handover loop that used to end at "claimed" with no way to arrange anything. - --- 1. Kinds for service exchanges. Additive: every existing row keeps its value. -ALTER TYPE "MarketplacePostKind" ADD VALUE IF NOT EXISTS 'OFFER_HELP'; -ALTER TYPE "MarketplacePostKind" ADD VALUE IF NOT EXISTS 'NEED_HELP'; - --- 2. Category enum -> text. `USING category::text` preserves every existing --- value verbatim (FURNITURE stays FURNITURE), so no row is reinterpreted. -ALTER TABLE "MarketplacePost" - ALTER COLUMN "category" DROP DEFAULT; - -ALTER TABLE "MarketplacePost" - ALTER COLUMN "category" TYPE TEXT USING "category"::text; - -ALTER TABLE "MarketplacePost" - ALTER COLUMN "category" SET DEFAULT 'OTHER'; - -DROP TYPE IF EXISTS "MarketplacePostCategory"; - --- 3. The handover. `contactNote` is how the two people actually meet — there --- is no resident-to-resident messaging in this product, so without it a --- match strands both sides. It is read back only to the claimer and staff. -ALTER TABLE "MarketplacePost" - ADD COLUMN IF NOT EXISTS "contactNote" TEXT, - ADD COLUMN IF NOT EXISTS "claimedAt" TIMESTAMP(3); - --- Existing claims have no timestamp to recover; the row's own updatedAt is the --- closest true statement, and leaving it NULL would read as "never claimed". -UPDATE "MarketplacePost" -SET "claimedAt" = "updatedAt" -WHERE "claimedById" IS NOT NULL AND "claimedAt" IS NULL; diff --git a/prisma/migrations/20260828100000_checkin_belongs_to_an_appointment/migration.sql b/prisma/migrations/20260828100000_checkin_belongs_to_an_appointment/migration.sql deleted file mode 100644 index b65d2ed1..00000000 --- a/prisma/migrations/20260828100000_checkin_belongs_to_an_appointment/migration.sql +++ /dev/null @@ -1,31 +0,0 @@ --- A satisfaction check-in can name the appointment it was collected in. --- --- Nullable: the resident's own portal rating and the deliberate full form are --- both legitimate and keep writing NULL here. Unique: an appointment is one --- conversation, so a second reading of the same meeting is a correction rather --- than a new fact. ON DELETE SET NULL because the check-in is the record of --- what someone said — removing the calendar entry must not remove that. - -ALTER TABLE "SatisfactionCheckIn" ADD COLUMN "appointmentId" TEXT; - -CREATE UNIQUE INDEX "SatisfactionCheckIn_appointmentId_key" - ON "SatisfactionCheckIn"("appointmentId"); - -ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_appointmentId_fkey" - FOREIGN KEY ("appointmentId") REFERENCES "Appointment"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- Who recorded it, as a key rather than as prose. --- --- "collectedBy" is free text a human types into the full form ("Team Nord"). --- It was briefly also used to hold a user id, so the column could contain --- either with no way to tell them apart. This splits the two: prose stays in --- "collectedBy", identity moves here. Existing rows keep their prose and get --- NULL here, which correctly reads as "we do not know which account". - -ALTER TABLE "SatisfactionCheckIn" ADD COLUMN "collectedByUserId" TEXT; - -CREATE INDEX "SatisfactionCheckIn_collectedByUserId_idx" - ON "SatisfactionCheckIn"("collectedByUserId"); - -ALTER TABLE "SatisfactionCheckIn" ADD CONSTRAINT "SatisfactionCheckIn_collectedByUserId_fkey" - FOREIGN KEY ("collectedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260828120000_staff_role_defaults_to_least_privilege/migration.sql b/prisma/migrations/20260828120000_staff_role_defaults_to_least_privilege/migration.sql deleted file mode 100644 index 61ea4ce9..00000000 --- a/prisma/migrations/20260828120000_staff_role_defaults_to_least_privilege/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Staff accounts default to the NARROWEST role, not the widest. --- --- "User"."role" defaulted to ADMIN (Leitung), so any row created without --- naming a role got every permission in the product. Combined with --- /api/auth/register hardcoding 'ADMIN', that is how all 23 staff accounts in --- production ended up as Leitung — which in turn meant the role system had no --- subjects to differentiate and every access boundary was a no-op. --- --- Existing rows are untouched: a column default applies only to new inserts. --- Re-roling the accounts that already exist is a decision about who may do --- what at AOZ, not a migration. - -ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'BETREUUNG'; diff --git a/prisma/migrations/20260828140000_employment_and_internship_kinds/migration.sql b/prisma/migrations/20260828140000_employment_and_internship_kinds/migration.sql deleted file mode 100644 index 0da233f6..00000000 --- a/prisma/migrations/20260828140000_employment_and_internship_kinds/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Work becomes something this product can hold. --- --- `Opportunity` had two kinds, VOLUNTEERING and COMMUNITY_SERVICE, both unpaid --- by definition. So the job coach role, the JOB care domain and the job_goal / --- work_status attributes all shipped while the object their work is about did --- not exist, and `PermitRequirement` — plainly built for employment — only ever --- decorated unpaid volunteering, the one case where it cannot matter. --- --- Added to BOTH enums in one migration on purpose: a started application --- becomes a LearningRecord of the same kind with no translation table, so a --- value present on one side and absent on the other fails as a Prisma enum --- error at the exact moment a coach is recording real work. - -ALTER TYPE "OpportunityKind" ADD VALUE IF NOT EXISTS 'EMPLOYMENT'; -ALTER TYPE "OpportunityKind" ADD VALUE IF NOT EXISTS 'INTERNSHIP'; - -ALTER TYPE "LearningKind" ADD VALUE IF NOT EXISTS 'EMPLOYMENT'; -ALTER TYPE "LearningKind" ADD VALUE IF NOT EXISTS 'INTERNSHIP'; diff --git a/prisma/migrations/20260828170000_resident_career_documents/migration.sql b/prisma/migrations/20260828170000_resident_career_documents/migration.sql deleted file mode 100644 index e38b3a86..00000000 --- a/prisma/migrations/20260828170000_resident_career_documents/migration.sql +++ /dev/null @@ -1,46 +0,0 @@ --- Career evidence a resident can show an employer: CV, certificate, reference. --- --- A job coach had nowhere to put a CV. The JOBCOACH role, the JOB care domain --- and the job_goal / work_status attributes all shipped while the artefact the --- work revolves around could not be attached to anyone. --- --- Bytes live in their own table so a list query that renders titles and dates --- never pulls megabytes — the same reason ResidentPhoto is separate from --- Resident. - -CREATE TABLE "ResidentDocument" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - "residentId" TEXT NOT NULL, - "category" TEXT NOT NULL DEFAULT 'OTHER', - "title" TEXT NOT NULL, - "fileName" TEXT NOT NULL, - "mimeType" TEXT NOT NULL, - "sizeBytes" INTEGER NOT NULL, - "uploadedByUserId" TEXT, - - CONSTRAINT "ResidentDocument_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "ResidentDocument_residentId_createdAt_idx" - ON "ResidentDocument"("residentId", "createdAt"); - --- Cascade: a deleted resident's file goes with them. -ALTER TABLE "ResidentDocument" ADD CONSTRAINT "ResidentDocument_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- SetNull: losing a staff account must not delete a resident's CV. Who --- uploaded it is metadata about the file, not ownership of it. -ALTER TABLE "ResidentDocument" ADD CONSTRAINT "ResidentDocument_uploadedByUserId_fkey" - FOREIGN KEY ("uploadedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - -CREATE TABLE "ResidentDocumentBlob" ( - "documentId" TEXT NOT NULL, - "data" BYTEA NOT NULL, - - CONSTRAINT "ResidentDocumentBlob_pkey" PRIMARY KEY ("documentId") -); - -ALTER TABLE "ResidentDocumentBlob" ADD CONSTRAINT "ResidentDocumentBlob_documentId_fkey" - FOREIGN KEY ("documentId") REFERENCES "ResidentDocument"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260829080000_residents_can_request_appointments/migration.sql b/prisma/migrations/20260829080000_residents_can_request_appointments/migration.sql deleted file mode 100644 index 78a744b8..00000000 --- a/prisma/migrations/20260829080000_residents_can_request_appointments/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ --- Residents can ask for a meeting, and staff can move one without cancelling it. --- --- Until now appointments were one-directional: staff scheduled, residents read. --- The one thing that structures a resident's relationship with the people --- responsible for them was the only surface they could not write to, while --- expenses, chores, reports and transfers all accept their input. --- --- Three changes, all additive: --- --- 1. REQUESTED — a meeting asked for and not yet answered. --- --- 2. "staffId" becomes NULLABLE. A resident asking for a meeting does not know --- who will take it, and on a deployment where nobody holds the care seat --- yet there is nobody to name. Requiring it would have made the feature --- dead on arrival for exactly the residents who need it. Existing rows all --- have a staff member and are untouched; the FK stays RESTRICT so removing --- a colleague still cannot silently orphan their calendar. --- --- 3. "residentNote" / "staffNote" — what was asked, and the answer that comes --- back. The second is the important one: a decision stored and never --- rendered is the same as no decision, which is the rule TransferRequest --- already follows. - -ALTER TYPE "AppointmentStatus" ADD VALUE IF NOT EXISTS 'REQUESTED'; - -ALTER TABLE "Appointment" ALTER COLUMN "staffId" DROP NOT NULL; - -ALTER TABLE "Appointment" ADD COLUMN "residentNote" TEXT; -ALTER TABLE "Appointment" ADD COLUMN "staffNote" TEXT; - --- An unclaimed request is the thing staff need to find; every other status is --- reached from a resident or a calendar view that is already indexed. -CREATE INDEX "Appointment_status_domain_idx" ON "Appointment"("status", "domain"); diff --git a/prisma/migrations/20260829100000_staff_scope_separate_from_role/migration.sql b/prisma/migrations/20260829100000_staff_scope_separate_from_role/migration.sql deleted file mode 100644 index b6290fbd..00000000 --- a/prisma/migrations/20260829100000_staff_scope_separate_from_role/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Separate three facts that were one enum. --- --- `StaffRole` answered three questions at once: which care domain do you work, --- how much do you see, and may you reconfigure the product. So the real AOZ --- setup could not be expressed: Franziska is a Betreuerin who ALSO sees every --- client, and the only way to grant that was to make her ADMIN — which erased --- her actual domain and handed her the settings page as a side effect. --- --- Now: role = domain, scope = breadth, isSystemAdmin = administration. --- --- Existing rows are migrated to keep behaviour byte-identical on day one: --- every current ADMIN gets ALL_DOMAINS + isSystemAdmin, which is exactly what --- ADMIN already meant. Nothing changes for anyone until an account is --- deliberately re-roled. - -CREATE TYPE "StaffScope" AS ENUM ('OWN_DOMAIN', 'ALL_DOMAINS'); - -ALTER TABLE "User" ADD COLUMN "scope" "StaffScope" NOT NULL DEFAULT 'OWN_DOMAIN'; -ALTER TABLE "User" ADD COLUMN "isSystemAdmin" BOOLEAN NOT NULL DEFAULT false; - --- Preserve what ADMIN meant, as data rather than as a role. -UPDATE "User" - SET "scope" = 'ALL_DOMAINS', "isSystemAdmin" = true - WHERE "role" = 'ADMIN'; - --- Finding the people who can see everything is the query an oversight change --- starts from; without this it is a full scan of a table that will grow. -CREATE INDEX "User_scope_idx" ON "User"("scope"); diff --git a/prisma/migrations/20260901020000_living_skills_support/migration.sql b/prisma/migrations/20260901020000_living_skills_support/migration.sql deleted file mode 100644 index 9d15b555..00000000 --- a/prisma/migrations/20260901020000_living_skills_support/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Wohnfähigkeit: how much help running a household a person needs. --- --- The City's owner strategy fixes "Berücksichtigung von vulnerablen Personen" --- as a minimum standard, and the Gemeinderat asked AOZ specifically for the --- "systematische Erfassung der Anzahl vulnerabler Geflüchteter". AOZ answered --- both on paper — a medical vulnerability assessment form in 2025, plus a --- separate project to record "Klient*innen mit eingeschränkten Wohnfähigkeiten --- sowie insbesondere ältere Geflüchtete" in order to close "bestehende --- Angebotslücken im Bereich Wohnen und Wohnbegleitung". --- --- Everything in that sentence except Wohnfähigkeit was already recordable here --- as a functional need. This column is the missing one. --- --- It is NOT a second `supportLevel`. That column is contact frequency; this is --- everyday competence — cooking, cleaning, post, appointments, keeping a --- tenancy. They come apart in both directions, which is why one cannot stand --- in for the other. --- --- INDEPENDENT is the default so no existing row acquires a support need nobody --- assessed. An unasked question must not read as an answer. -CREATE TYPE "LivingSkillsSupport" AS ENUM ('INDEPENDENT', 'SOME_SUPPORT', 'REGULAR_SUPPORT'); - -ALTER TABLE "Resident" - ADD COLUMN "livingSkillsSupport" "LivingSkillsSupport" NOT NULL DEFAULT 'INDEPENDENT'; - --- The reportable figure is a COUNT of people needing more than the default, so --- the index serves the one query this column exists to answer. -CREATE INDEX "Resident_livingSkillsSupport_idx" ON "Resident"("livingSkillsSupport"); diff --git a/prisma/migrations/20260901090000_complaints_about_the_organisation/migration.sql b/prisma/migrations/20260901090000_complaints_about_the_organisation/migration.sql deleted file mode 100644 index 18f38f6c..00000000 --- a/prisma/migrations/20260901090000_complaints_about_the_organisation/migration.sql +++ /dev/null @@ -1,52 +0,0 @@ --- A channel that points at the organisation, not at the resident. --- --- The report form has offered exactly two destinations: a dripping tap to the --- maintenance board, a roommate conflict to the incident ladder. An objection --- to how AOZ itself acted fitted neither, and filing it as an Incident would --- have been worse than dropping it: that ladder escalates TOWARD a resident and --- ends in FORMAL_MEASURE, so complaining about staff would have opened a case --- against the person complaining. --- --- The City's Eigentümerstrategie 2025-2028 fixes "Information und --- Beschwerdestellen" as one of six minimum standards in AOZ's Leistungsauftrag. --- AOZ's own central Beschwerdestelle logged 88 complaints in 2023, 145 in 2024 --- and 242 in 2025 — 38% of the last figure about Unterbringung und --- Zusammenleben, while the client count stayed flat. The product had no side of --- that obligation at all. -CREATE TYPE "ComplaintSubject" AS ENUM ('STAFF', 'ACCOMMODATION', 'DECISION', 'OTHER'); -CREATE TYPE "ComplaintStatus" AS ENUM ('OPEN', 'IN_REVIEW', 'ANSWERED'); - -CREATE TABLE "Complaint" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - -- NULL = filed anonymously. Someone objecting to the organisation that - -- houses them pays a cost for being identifiable, so anonymity has to be on - -- offer. SET NULL rather than CASCADE on purpose: a complaint must outlive - -- the reporter's record, or deleting a person would erase what they said - -- about the service. - "residentId" TEXT, - - "subject" "ComplaintSubject" NOT NULL, - "body" TEXT NOT NULL, - "status" "ComplaintStatus" NOT NULL DEFAULT 'OPEN', - - "response" TEXT, - "respondedAt" TIMESTAMP(3), - "respondedByUserId" TEXT, - - CONSTRAINT "Complaint_pkey" PRIMARY KEY ("id") -); - -ALTER TABLE "Complaint" - ADD CONSTRAINT "Complaint_residentId_fkey" - FOREIGN KEY ("residentId") REFERENCES "Resident"("id") ON DELETE SET NULL ON UPDATE CASCADE; - -ALTER TABLE "Complaint" - ADD CONSTRAINT "Complaint_respondedByUserId_fkey" - FOREIGN KEY ("respondedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; - -CREATE INDEX "Complaint_status_idx" ON "Complaint"("status"); -CREATE INDEX "Complaint_residentId_idx" ON "Complaint"("residentId"); -CREATE INDEX "Complaint_createdAt_idx" ON "Complaint"("createdAt"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 99e4f200..00000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index d3cce7b9..00000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,2408 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - directUrl = env("DIRECT_URL") -} - -// ============================================================================= -// RESIDENTS -// ============================================================================= - -model Resident { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Basic info (non-identifying) - code String @unique // Internal reference code, not name - ageRange AgeRange - gender Gender - familyStatus FamilyStatus - - // Lifestyle preferences - sleepSchedule SleepSchedule - noiseTolerance Int // 1-5 scale - guestTolerance Int @default(3) // 1-5 scale - - // Cleanliness — three separate things that a single "cleanliness level" hides. - // Conflicts come from the gap BETWEEN people, and that gap is directional: - // what I expect of you, measured against what you actually do, softened by - // how much mess I can live with. Two tidy-but-relaxed people get along; one - // demanding person next to one relaxed person does not — even when both are - // "medium" on a single combined scale. - cleanlinessPractice Int // 1-5: how much cleanliness this person keeps themselves - cleanlinessExpectation Int @default(3) // 1-5: how much they expect from others - chaosTolerance Int @default(3) // 1-5: how much mess/disorder they can live with - - // Social factors - socialStyle SocialStyle - languages String[] // ISO language codes - culturalRegion String? // Broad region, not country - conflictStyle ConflictStyle @default(COOPERATIVE) - - // Practical needs - smokingStatus SmokingStatus - dietaryNeeds String[] // e.g., ["halal", "vegetarian"] - mobilityNeeds MobilityNeed - medicalEquipment Boolean @default(false) // Needs space for equipment - - // Preferences - petTolerance Boolean @default(true) - sharedBathroom Boolean @default(true) - sharedKitchen Boolean @default(true) - privacyNeed Int // 1-5 scale - - // Household (chores, recycling) - choresContribution Int @default(3) // 1-5 scale: contribution to shared tasks - recyclingKnowledge RecyclingKnowledge @default(NONE) - - // Health/Support needs (functional, not diagnostic) - roomSharingStatus RoomSharingStatus @default(CAN_SHARE) - hasNightDisturbances Boolean @default(false) // Nightmares, sleep talking - needsQuietEnvironment Boolean @default(false) // Anxiety, sensory needs - hasSleepEquipment Boolean @default(false) // CPAP, etc. - supportLevel SupportLevel @default(STANDARD) - - /// Whether conversations with this person need an interpreter. - /// - /// The LEVEL only. Which language is already on `languages`, and a second - /// column naming it would be a second source of truth that drifts. - /// - /// This is not a language test and not a judgement about someone's German — - /// `germanLevel` is a separate, learning-domain fact. It answers one - /// operational question: does booking this meeting require booking a third - /// person too. @see lib/config/interpreting.ts - interpreterNeed InterpreterNeed @default(NONE) - - /// How much help running a household this person needs — AOZ's - /// "Wohnfähigkeit". Distinct from `supportLevel`, which is how often someone - /// checks in on them: a person can need weekly contact and still cook, clean - /// and open their post unaided, and another can be seen rarely and still be - /// unable to keep a tenancy. Conflating them is why AOZ was recording this on - /// paper in a separate project rather than reading it off a system. - /// - /// FUNCTIONAL, never a diagnosis: it says what support the household needs, - /// not why. @see lib/vulnerability/ - livingSkillsSupport LivingSkillsSupport @default(INDEPENDENT) - - // Roommate preferences (from portal self-service) - roommatePreferences String? - preferencesCompletedAt DateTime? // Set when portal preferences form is submitted - - // Self-chosen identity (portal profile). The code stays the login identity - // and the staff-facing reference; displayName/bio/photo are OPTIONAL and - // owned by the resident — never entered by staff, never required. - displayName String? - bio String? - photo ResidentPhoto? - documents ResidentDocument[] - - /// Who may see displayName, bio and photo. Staff always can — they have to - /// be able to identify the person they are supporting. This governs what - /// OTHER RESIDENTS see. Default ROOMMATES preserves the behaviour the photo - /// endpoint already had, so nobody's visibility widens on migration. - profileVisibility ProfileVisibility @default(ROOMMATES) - - complaints Complaint[] - messageThread MessageThread? - messagesWritten Message[] @relation("MessageAuthor") - - // Login credentials live on Account, never here — one human may hold both a - // staff account and a resident record (a caseworker who also lives in a - // shared flat), and two password hashes for one human WILL drift. - account Account? - - // Status - status ResidentStatus @default(ACTIVE) - notes String? // Caseworker notes - - // Medical documentation for private placement eligibility - hasMedicalDocumentation Boolean @default(false) - medicalDocType MedicalDocType? - medicalDocDate DateTime? - medicalDocNotes String? - - // Relations - placements Placement[] - assessments CompatibilityAssessment[] @relation("ResidentAssessments") - comparedWith CompatibilityAssessment[] @relation("ComparedResidentAssessments") - incidentsReported Incident[] @relation("IncidentReporter") - incidentsAsSubject Incident[] @relation("IncidentSubject") - incidentInvolvements IncidentInvolvement[] - maintenanceRequests MaintenanceRequest[] // Already has onDelete: SetNull on FK side - - // Household tasks - createdTasks HouseholdTask[] @relation("TaskCreator") - taskCompletions TaskCompletion[] - taskAttentionFlags TaskAttentionFlag[] - taskRequestsMade TaskRequest[] @relation("TaskRequestsMade") - taskRequestsReceived TaskRequest[] @relation("TaskRequestsReceived") - - // Transfer requests - transferRequests TransferRequest[] - - // Governance — house rules, decisions, conflict agreements - ruleAcknowledgements RuleAcknowledgement[] - proposalsMade Proposal[] @relation("ProposalAuthor") - votes Vote[] - agreementParties AgreementParty[] - - // Shared expenses - expensesPaid Expense[] @relation("ExpensePayer") - expensesCreated Expense[] @relation("ExpenseCreator") - expenseShares ExpenseShare[] - settlementsPaid Settlement[] @relation("SettlementFrom") - settlementsRecvd Settlement[] @relation("SettlementTo") - - // Learning — language tests, courses, informal learning. Functional, never - // a diagnosis. Social workers and job coaches read this; residents own it. - learningRecords LearningRecord[] - careAssignments CareAssignment[] - appointments Appointment[] - careAttributes CareAttribute[] - - // What someone COULD do next. LearningRecord is the retrospective half of - // the same story; this is the forward-looking one. - opportunityApplications OpportunityApplication[] - - // Marketplace & events - marketplacePostsCreated MarketplacePost[] @relation("MarketplacePostedBy") - marketplacePostsClaimed MarketplacePost[] @relation("MarketplacePostClaimedBy") - houseEventsCreated HouseEvent[] @relation("HouseEventCreatedByResident") - eventRsvps EventRsvp[] - - @@index([status]) - @@index([ageRange, gender]) -} - -// Avatar photo, one per resident. Separate table on purpose: Prisma selects -// all scalar columns by default, so Bytes on Resident would drag the image -// into every findMany. Kept out of the row, joined only by the photo route. -model ResidentPhoto { - residentId String @id - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - data Bytes - mimeType String - updatedAt DateTime @updatedAt -} - -/// Career evidence a resident can show an employer — a CV, a certificate, a -/// reference. Added because a job coach had nowhere to put a CV: the role, the -/// JOB care domain and the job_goal attribute all shipped while the artefact -/// the work revolves around could not be attached to anyone. -/// -/// Deliberately NOT a general document store. What may go here is bounded by -/// DOCUMENT_CATEGORIES in lib/config/documents.ts, and the rules CLAUDE.md -/// already sets still hold: no medical documents, no asylum paperwork, no -/// permit scans. This must not become the place those accumulate. -model ResidentDocument { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - /// A category id from lib/config/documents.ts, NOT a database enum — the - /// same rule the marketplace categories follow. Vocabulary is config; - /// adding "Diplom" must be one line, never a migration. - category String @default("OTHER") - - title String - fileName String - mimeType String - sizeBytes Int - - /// SetNull, not Cascade: losing a staff account must not delete a resident's - /// CV. The person who uploaded it is metadata about the file, not its owner. - uploadedBy User? @relation("DocumentUploadedBy", fields: [uploadedByUserId], references: [id], onDelete: SetNull) - uploadedByUserId String? - - blob ResidentDocumentBlob? - - @@index([residentId, createdAt]) -} - -/// The bytes, in their own table so they never ride along on a list query. -/// Same reason ResidentPhoto is separate: a document list renders titles and -/// dates, and pulling megabytes to render a filename is the kind of cost that -/// only shows up once the table is full. -model ResidentDocumentBlob { - documentId String @id - document ResidentDocument @relation(fields: [documentId], references: [id], onDelete: Cascade) - data Bytes -} - -/// A conversation between one resident and the staff team. -/// -/// One thread per resident, not per topic. A resident with a question does not -/// know which of their concerns counts as "the same conversation" as another, -/// and making them pick a thread is asking them to do the filing for us. Staff -/// get the whole history with that person in one place, which is also how they -/// avoid asking something a colleague already answered. -/// A complaint about the ORGANISATION — never about a roommate. -/// -/// The report form already routes a dripping tap to the maintenance board and -/// a roommate conflict to the incident ladder. Neither fits an objection to how -/// AOZ itself acted, and filing one as an Incident would be actively harmful: -/// that ladder escalates TOWARD a resident and ends in FORMAL_MEASURE, so -/// complaining about staff would open a case against the person complaining. -/// -/// The City's Eigentümerstrategie fixes "Information und Beschwerdestellen" as -/// a minimum standard, and AOZ runs a central Beschwerdestelle that logged 242 -/// complaints in 2025 — 38% of them about Unterbringung und Zusammenleben. This -/// table is the product's side of that obligation. -/// -/// @see lib/auth/role-policy.ts — COMPLAINT_PERMISSIONS, and why oversight -/// over every care domain deliberately does NOT grant them. -model Complaint { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - /// NULL when filed anonymously. A person objecting to the organisation that - /// houses them is in a position where being identifiable is itself a cost, - /// so anonymity has to be available — the same reason the satisfaction - /// check-in is anonymous. The trade-off is real and stated in the form: - /// nobody can write back to an anonymous complaint, and it cannot appear in - /// "Deine Meldungen". - residentId String? - resident Resident? @relation(fields: [residentId], references: [id], onDelete: SetNull) - - subject ComplaintSubject - body String - - status ComplaintStatus @default(OPEN) - - /// What was written back. Shown to the resident in their own report list — - /// an answer stored and never rendered is the same as no answer. - response String? - respondedAt DateTime? - respondedByUserId String? - respondedBy User? @relation("ComplaintRespondedBy", fields: [respondedByUserId], references: [id], onDelete: SetNull) - - @@index([status]) - @@index([residentId]) - @@index([createdAt]) -} - -/// What the complaint is about. Deliberately coarse: a complaints form is not -/// a taxonomy exercise, and a resident should not have to classify their own -/// grievance precisely before being allowed to make it. -enum ComplaintSubject { - STAFF // How I was treated by someone working here - ACCOMMODATION // The accommodation itself, or its rules - DECISION // A decision that was made about me - OTHER -} - -enum ComplaintStatus { - OPEN - IN_REVIEW - ANSWERED -} - -model MessageThread { - id String @id @default(cuid()) - residentId String @unique - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - /// Bumped on every message, so a staff inbox can sort by "waiting longest" - /// without loading every thread's messages. - updatedAt DateTime @updatedAt - - messages Message[] - - @@index([updatedAt]) -} - -/// One message. Either a resident wrote it or a staff member did — never both -/// and never neither, which is what `messageAuthor()` and a CHECK constraint -/// enforce from either end. -model Message { - id String @id @default(cuid()) - threadId String - thread MessageThread @relation(fields: [threadId], references: [id], onDelete: Cascade) - - /// Set when a resident wrote it. Restrict, not Cascade: deleting a person - /// must never silently rewrite a conversation staff may have to account for. - authorResidentId String? - authorResident Resident? @relation("MessageAuthor", fields: [authorResidentId], references: [id], onDelete: Restrict) - - /// Set when a staff member wrote it. - authorUserId String? - authorUser User? @relation("MessageAuthor", fields: [authorUserId], references: [id], onDelete: Restrict) - - body String - createdAt DateTime @default(now()) - - /// When the OTHER side read it. Null means unread. One timestamp is enough - /// because a thread has exactly two sides. - readAt DateTime? - - @@index([threadId, createdAt]) -} - -/// How far a resident's self-chosen profile reaches among other residents. -enum ProfileVisibility { - /// Only the resident and staff. - PRIVATE - /// Also the people they actually live with. - ROOMMATES - /// Also every other resident in the system. - RESIDENTS -} - -enum AgeRange { - YOUNG_ADULT // 18-25 - ADULT // 26-40 - MIDDLE_AGED // 41-55 - SENIOR // 56+ -} - -enum Gender { - MALE - FEMALE - OTHER - PREFER_NOT_SAY -} - -enum FamilyStatus { - SINGLE - COUPLE - FAMILY_WITH_CHILDREN - SINGLE_PARENT -} - -enum SleepSchedule { - EARLY_BIRD // Sleep before 22:00, wake before 06:00 - STANDARD // Sleep 22:00-24:00, wake 06:00-08:00 - NIGHT_OWL // Sleep after 24:00, wake after 08:00 - IRREGULAR // Shift work or variable -} - -enum SocialStyle { - INTROVERTED // Prefers solitude, minimal interaction - MODERATE // Balanced social needs - EXTROVERTED // Enjoys frequent interaction -} - -enum SmokingStatus { - NON_SMOKER - OUTDOOR_SMOKER // Smokes but only outside - INDOOR_SMOKER // Needs to smoke indoors -} - -enum MobilityNeed { - NONE - GROUND_FLOOR // Cannot use stairs - WHEELCHAIR // Full accessibility required -} - -enum ResidentStatus { - ACTIVE // Currently in system - PLACED // Has housing assignment - TRANSFERRED // Moved to different housing - EXITED // Left the system -} - -enum MedicalDocType { - PRIVATE_ROOM // Qualifies for single room - STUDIO // Qualifies for studio/apartment - BOTH // Qualifies for either -} - -enum RoomSharingStatus { - CAN_SHARE // Default - can share room - PREFERS_PRIVATE // Would prefer single but can manage - NEEDS_PRIVATE // Cannot share (medical/psychological reason) -} - -enum SupportLevel { - STANDARD // Normal check-ins - ELEVATED // More frequent monitoring - INTENSIVE // Close support needed -} - -/// Whether a conversation with this person needs an interpreter booked. -/// -/// AOZ runs Medios: ~80 languages, ~1000 interpreters, booked on an external -/// platform with roughly a day's confirmation lead time. This product does not -/// rebuild that booking and should not — it records the NEED, so that a -/// meeting is not scheduled for tomorrow morning with nobody able to speak to -/// the person once it starts. -enum InterpreterNeed { - /// Manages in a language the team shares. - NONE - /// Helpful for official, complex or consequential conversations. - FOR_COMPLEX - /// Needed for any substantive conversation. - ALWAYS -} - -/// How much support running a household this person needs — AOZ's -/// "Wohnfähigkeit". A capability, never a diagnosis: the values say what help -/// the household needs, and nothing about why it is needed. -/// -/// Deliberately separate from `SupportLevel`. That one is contact FREQUENCY; -/// this is everyday competence with cooking, cleaning, post, appointments and -/// keeping a tenancy. The two come apart in both directions, and AOZ named -/// "Klient*innen mit eingeschränkten Wohnfähigkeiten" as a group it could not -/// find in any system. -enum LivingSkillsSupport { - INDEPENDENT // Runs the household unaided - SOME_SUPPORT // Occasional help: post, appointments, paperwork - REGULAR_SUPPORT // Ongoing accompaniment in daily living -} - -enum RecyclingKnowledge { - NONE // No experience with Swiss recycling - BASIC // Knows basics (paper, glass, PET) - GOOD // Understands full system (compost, textiles, etc.) -} - -enum ConflictStyle { - AVOIDANT // Avoids confrontation - COOPERATIVE // Seeks compromise - DIRECT // Addresses issues head-on -} - -// ============================================================================= -// HOUSING -// ============================================================================= - -model HousingUnit { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Identification - code String @unique // Internal reference - address String - - // Resident-chosen apartment name (e.g. "Singapur"). Set from the portal by - // the people who live there; staff views show it next to the address. - nickname String? - - // Capacity - totalBeds Int - totalRooms Int - sharedRooms Int // Rooms with multiple beds - privateRooms Int // Single-occupancy rooms - - // Facilities - sharedBathrooms Int - privateBathrooms Int - sharedKitchen Boolean @default(true) - privateKitchen Boolean @default(false) - - // Accessibility - groundFloor Boolean @default(false) - wheelchairAccess Boolean @default(false) - elevator Boolean @default(false) - - // Rules - smokingAllowed Boolean @default(false) - petsAllowed Boolean @default(false) - quietHours String? // e.g., "22:00-07:00" - - // Location factors - nearPublicTransport Boolean @default(true) - nearHealthServices Boolean @default(false) - nearSchools Boolean @default(false) - - // Optional grouping for a Standort with many apartments. - // A full Building table is H2; a code is enough to sort and filter today. - buildingCode String? - - // Status - status HousingStatus @default(AVAILABLE) - notes String? - - // Relations - spots PlacementSpot[] - placements Placement[] - incidents Incident[] - maintenanceRequests MaintenanceRequest[] - householdTasks HouseholdTask[] - transferRequests TransferRequest[] - marketplacePosts MarketplacePost[] - houseEvents HouseEvent[] - - /// Staff explicitly responsible for this unit. @see StaffUnit - staffAccess StaffUnit[] - - // Governance - houseRules HouseRule[] - proposals Proposal[] - - // Shared expenses - expenses Expense[] - settlements Settlement[] - - @@index([status]) - @@index([totalBeds]) - @@index([buildingCode]) -} - -// ============================================================================= -// SHARED EXPENSES -// ============================================================================= -// Money is stored as INTEGER Rappen — exact arithmetic, no float drift. -// Invariant (enforced in lib/expenses, tested): sum(shares) === amountRappen. -// Resident FKs are Restrict, not Cascade: deleting a payer would silently -// change everyone else's balance. Residents exit via status, not deletion. - -model Expense { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - housingUnitId String - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - - // Who paid, and who recorded it (may differ: any roommate can log a - // purchase on behalf of the person who paid). - paidById String - paidBy Resident @relation("ExpensePayer", fields: [paidById], references: [id], onDelete: Restrict) - createdById String - createdBy Resident @relation("ExpenseCreator", fields: [createdById], references: [id], onDelete: Restrict) - - description String - category String // Validated against EXPENSE_CATEGORIES config — string, not enum, so a new category is a config change, not a migration - amountRappen Int - date DateTime - - shares ExpenseShare[] - - @@index([housingUnitId, date]) -} - -model ExpenseShare { - id String @id @default(cuid()) - - expenseId String - expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade) - - residentId String - resident Resident @relation(fields: [residentId], references: [id], onDelete: Restrict) - - amountRappen Int - - @@unique([expenseId, residentId]) - @@index([residentId]) -} - -// A direct payment between roommates that settles (part of) a debt. -model Settlement { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - housingUnitId String - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - - fromId String - from Resident @relation("SettlementFrom", fields: [fromId], references: [id], onDelete: Restrict) - toId String - to Resident @relation("SettlementTo", fields: [toId], references: [id], onDelete: Restrict) - - amountRappen Int - note String? - - @@index([housingUnitId]) -} - -enum HousingStatus { - AVAILABLE // Open for placements - FULL // At capacity - MAINTENANCE // Temporarily unavailable - CLOSED // Permanently closed -} - -// ============================================================================= -// PLACEMENT SPOTS (Beds, Rooms, Studios) -// ============================================================================= - -model PlacementSpot { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Location - housingUnitId String - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - - // Identification - code String // "R1-B1", "R2", "STUDIO-A" - label String? // Human-friendly name - - // Type determines what this spot is - type SpotType - - // Hierarchy (for beds within rooms) - parentSpotId String? - parentSpot PlacementSpot? @relation("SpotHierarchy", fields: [parentSpotId], references: [id], onDelete: Cascade) - childSpots PlacementSpot[] @relation("SpotHierarchy") - - // Physical attributes - squareMeters Float? - floor Int? - - // Facilities (mainly for PRIVATE_ROOM and STUDIO) - hasPrivateBathroom Boolean @default(false) - hasPrivateKitchen Boolean @default(false) - hasPrivateToilet Boolean @default(false) - - // Capacity (beds per room, or 1 for single spots) - capacity Int @default(1) - - // Eligibility - requiresMedicalDocs Boolean @default(false) - - // Status - status SpotStatus @default(AVAILABLE) - notes String? - - // Relations - placements Placement[] - maintenanceRequests MaintenanceRequest[] - - @@unique([housingUnitId, code]) - @@index([type, status]) - @@index([requiresMedicalDocs]) - @@index([housingUnitId]) -} - -enum SpotType { - BED // Individual bed in shared room - PRIVATE_ROOM // Single-occupancy room (medical) - STUDIO // Self-contained unit (medical) - ROOM // Container for beds (not directly assignable) -} - -enum SpotStatus { - AVAILABLE // Can be assigned - OCCUPIED // Currently has placement - MAINTENANCE // Temporarily unavailable - CLOSED // Permanently unavailable -} - -// ============================================================================= -// PLACEMENTS -// ============================================================================= - -model Placement { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Assignment - resident Resident @relation(fields: [residentId], references: [id], onDelete: Restrict) - residentId String - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Restrict) - housingUnitId String - - // Specific spot within the unit (bed, room, or studio) - spot PlacementSpot? @relation(fields: [spotId], references: [id], onDelete: SetNull) - spotId String? - - // Dates - startDate DateTime - endDate DateTime? - - // Compatibility at placement time - compatibilityScore Float? // 0-100 overall score - lifestyleScore Float? // Dimension scores - socialScore Float? - practicalScore Float? - riskScore Float? - - // Outcome tracking - status PlacementStatus @default(ACTIVE) - endReason EndReason? - satisfactionRating Int? // 1-5, self-reported - - // Notes - placementNotes String? // Why this placement was chosen - outcomeNotes String? // How it went - - // Conflict analysis (captured when endReason is CONFLICT) - conflictGap String? // Which compatibility dimension failed (NOISE, CLEANLINESS, etc.) - wasPredictable Boolean? // Was this predictable from initial compatibility score? - relatedIncidentId String? // Optional link to related incident - relatedIncident Incident? @relation("PlacementConflictIncident", fields: [relatedIncidentId], references: [id], onDelete: SetNull) - - // Relations - incidents Incident[] - checkIns SatisfactionCheckIn[] - transferRequests TransferRequest[] @relation("TransferFromPlacement") - - @@unique([residentId, housingUnitId, startDate]) - @@index([status]) - @@index([startDate, endDate]) - @@index([residentId]) - @@index([housingUnitId]) -} - -enum PlacementStatus { - ACTIVE - ENDED - TRANSFERRED -} - -enum EndReason { - NATURAL // Normal exit from system - CONFLICT // Due to roommate issues - REQUEST // Resident requested move - CAPACITY // Housing capacity change - UPGRADE // Moved to better situation - OTHER -} - -// ============================================================================= -// COMPATIBILITY -// ============================================================================= - -model CompatibilityAssessment { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // Compared residents - resident Resident @relation("ResidentAssessments", fields: [residentId], references: [id], onDelete: Cascade) - residentId String - comparedWith Resident @relation("ComparedResidentAssessments", fields: [comparedWithId], references: [id], onDelete: Cascade) - comparedWithId String - - // Scores (0-100) - overallScore Float - lifestyleScore Float - socialScore Float - practicalScore Float - riskScore Float // Lower is better (inverse of compatibility) - - // Details - strengths String[] // What makes them compatible - concerns String[] // Potential issues - recommendations String[] // Mitigations if placed together - - @@unique([residentId, comparedWithId]) - @@index([overallScore]) -} - -// ============================================================================= -// INCIDENTS -// ============================================================================= - -model Incident { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Location - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Restrict) - housingUnitId String - placement Placement? @relation(fields: [placementId], references: [id], onDelete: SetNull) - placementId String? - - // Attribution - WHO reported this incident - reportedBy Resident? @relation("IncidentReporter", fields: [reportedById], references: [id], onDelete: SetNull) - reportedById String? - - // Attribution - WHO the incident is about (the troublemaker/affected party) - subject Resident? @relation("IncidentSubject", fields: [subjectId], references: [id], onDelete: SetNull) - subjectId String? - - // For multi-party conflicts - involvedResidents IncidentInvolvement[] - - // Details - date DateTime - category IncidentCategory @default(INTERPERSONAL) - type IncidentType - severity IncidentSeverity - description String - resolution String? - resolvedAt DateTime? - mediationMinutes Int? // Staff time spent mediating this incident (for ROI tracking) - - // For algorithm improvement - predictable Boolean? // Could this have been predicted? - compatibilityGap String? // Which compatibility dimension failed? - - // Follow-up tracking - followUps IncidentFollowUp[] - nextFollowUpDate DateTime? // Scheduled next follow-up - followUpPriority FollowUpPriority? // How urgent is follow-up - - // Conflict-resolution ladder: which step this conflict is currently on. - // The ladder starts with the people involved and only escalates when a step - // does not hold — staff time goes to the cases that actually need it. - resolutionStage ResolutionStage @default(REPORTED) - stageEnteredAt DateTime @default(now()) - agreements ConflictAgreement[] - - // Reverse relation for placements ended due to this incident - conflictPlacements Placement[] @relation("PlacementConflictIncident") - - @@index([type, severity]) - @@index([date]) - @@index([reportedById]) - @@index([subjectId]) - @@index([nextFollowUpDate]) -} - -model IncidentFollowUp { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade) - incidentId String - - // Content - action String // What action was taken - notes String? // Additional notes - outcome String? // Result of the follow-up - - // Who did the follow-up - staffName String? // Caseworker name - - // Scheduling - scheduledNextDate DateTime? // If another follow-up is needed - - @@index([incidentId]) - @@index([createdAt]) -} - -enum FollowUpPriority { - LOW // Check within a week - NORMAL // Check within 2-3 days - HIGH // Check within 24 hours - URGENT // Check today -} - -model IncidentInvolvement { - id String @id @default(cuid()) - incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade) - incidentId String - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - role InvolvementRole @default(INVOLVED) - - @@unique([incidentId, residentId]) - @@index([residentId]) -} - -enum InvolvementRole { - INVOLVED // Party in the conflict - WITNESS // Witnessed the incident - MEDIATOR // Helped resolve -} - -enum IncidentType { - // Interpersonal - NOISE_COMPLAINT - CLEANLINESS_DISPUTE - PERSONAL_CONFLICT - CULTURAL_FRICTION - SPACE_DISPUTE - SCHEDULE_CONFLICT - SAFETY_CONCERN - - // Maintenance/Facility - PLUMBING - ELECTRICAL - HEATING_COOLING - APPLIANCE - STRUCTURAL - PEST_CONTROL - SECURITY_SYSTEM - GENERAL_MAINTENANCE - - // Wellbeing/Satisfaction - LOW_SATISFACTION // Auto-generated from portal check-in - - OTHER -} - -enum IncidentCategory { - INTERPERSONAL // Conflicts between residents - MAINTENANCE // Facility/equipment issues - SAFETY // Safety-related (could be either) - WELLBEING // Satisfaction/wellbeing concerns -} - -enum IncidentSeverity { - LOW // Minor, resolved quickly - MEDIUM // Required intervention - HIGH // Serious, may require transfer - CRITICAL // Immediate action needed -} - -// ============================================================================= -// SATISFACTION TRACKING -// ============================================================================= - -model SatisfactionCheckIn { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // Who and where - placement Placement @relation(fields: [placementId], references: [id], onDelete: Cascade) - placementId String - - // Timing - checkInType CheckInType - weekNumber Int? // Week number since placement start - - // Ratings (all 1-5) - overallSatisfaction Int // How happy overall? - roommateRelations Int? // How are relations with roommates? - facilitySatisfaction Int? // How satisfied with the unit itself? - safetyFeeling Int? // How safe do they feel? - - // Qualitative - concerns String? // Any specific concerns? - improvements String? // What could be better? - positives String? // What's working well? - - // Meta - /// Free text, typed by whoever fills the full form ("Team Nord", a name). - /// It is a note, not an identifier, and must never be read as one. - collectedBy String? - - /// The signed-in staff member who recorded this. Null means self-reported: - /// the resident rated their own week in the portal. - /// - /// Separate from `collectedBy` because that field is prose a human types and - /// this one is a foreign key. They were briefly the same column, which made - /// it hold either a typed name or a user id with no way to tell which — a - /// column that means two things answers neither question. - /// - /// SetNull, not Cascade: removing a staff account must not delete the record - /// of what a resident said. - collectedByUser User? @relation("CheckInCollectedBy", fields: [collectedByUserId], references: [id], onDelete: SetNull) - collectedByUserId String? - - isAnonymous Boolean @default(false) - - /// The appointment this was collected in, when it was collected in one. - /// - /// Staff used to rate a resident's mood from an always-present widget on the - /// client page, detached from any interaction — so the record said what - /// someone felt but never when, or in the course of what. `collectedBy` - /// answers "who typed it"; this answers "on what occasion", which is the - /// half that makes the number interpretable later. - /// - /// Nullable because the two other paths are legitimate and stay: the - /// resident's own portal rating, and the full form a caseworker fills in - /// deliberately. Unique because an appointment is one conversation — a - /// second reading of the same meeting is a correction, not a new fact. - appointment Appointment? @relation(fields: [appointmentId], references: [id], onDelete: SetNull) - appointmentId String? @unique - - @@index([placementId]) - @@index([checkInType]) -} - -enum CheckInType { - INITIAL // First week check-in - REGULAR // Scheduled periodic check-in - AD_HOC // Unscheduled check-in - EXIT // When placement ends -} - -// ============================================================================= -// SYSTEM -// ============================================================================= - -model AlgorithmWeight { - id String @id @default(cuid()) - updatedAt DateTime @updatedAt - - // Dimension weights (should sum to 100) - lifestyleWeight Float @default(30) - socialWeight Float @default(25) - practicalWeight Float @default(25) - riskWeight Float @default(20) - - // Individual factor weights within dimensions - factorWeights Json // Detailed weights per factor - - // Versioning - version Int @default(1) - active Boolean @default(true) - notes String? - - @@index([active]) -} - -// ============================================================================= -// AUTHENTICATION (prepared for future implementation) -// ============================================================================= - -model User { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - messagesWritten Message[] @relation("MessageAuthor") - - // Identity - code String @unique // Staff login code (works forever) - name String - - // Role & Status - /// Least privilege by default. This defaulted to ADMIN — so any row created - /// without naming a role became Leitung, the widest role in the product. - /// Every caller sets it explicitly (seed-admin, invites, the demo doors), so - /// nothing relies on the default; it existed only to be forgotten. - /// WHICH CARE DOMAIN this person is staffed for — one fact, nothing else. - /// Breadth of view lives in `scope`; product administration in - /// `isSystemAdmin`. Those were all one enum, which is why "a Betreuerin who - /// also sees everything" was unexpressible without making her an admin. - role StaffRole @default(BETREUUNG) - - /// How far this person can see. Orthogonal to `role`: the real Betreuerin - /// at AOZ works the housing domain AND sees every client, and a job coach - /// works one domain and sees only that. - scope StaffScope @default(OWN_DOMAIN) - - /// WHICH PLACES this person is responsible for. - /// - /// A fourth orthogonal fact, and the one the three-axis model could not say. - /// `role` is which care domain, `scope` is how many domains, and neither - /// answers "which houses". AOZ runs 31 sites (16 in the city), the portfolio - /// turns over every few years, and it staffs a `Springer*in` explicitly - /// described as having "kein fester Arbeitsort" — one person covering some - /// sites but not others, which nothing here could express. - /// - /// ALL_UNITS is the default so NOBODY's access changes the day this ships: - /// every existing row keeps seeing everything, exactly as the ADMIN→scope - /// migration did. Narrowing someone is then a deliberate act, never a - /// side effect of a deploy. - siteAccess SiteAccess @default(ALL_UNITS) - /// Which units, when `siteAccess` is ASSIGNED_UNITS. Ignored otherwise. - unitAccess StaffUnit[] - - /// May configure the product: invite staff, change settings, import data. - /// NOT implied by seeing everything — reading every file and reconfiguring - /// the system are different jobs, and conflating them is what forced every - /// oversight grant to also hand over the settings page. - isSystemAdmin Boolean @default(false) - - active Boolean @default(true) - lastLoginAt DateTime? - - // Relations - auditLogs AuditLog[] - activitiesCreated Activity[] @relation("ActivityCreatedBy") - activitiesUpdated Activity[] @relation("ActivityUpdatedBy") - careAssignments CareAssignment[] - appointments Appointment[] - careAttributesUpdated CareAttribute[] - houseEventsCreated HouseEvent[] @relation("HouseEventCreatedByStaff") - opportunitiesCreated Opportunity[] @relation("OpportunityCreatedBy") - opportunitiesUpdated Opportunity[] @relation("OpportunityUpdatedBy") - applicationsSupported OpportunityApplication[] @relation("ApplicationSupportedBy") - checkInsCollected SatisfactionCheckIn[] @relation("CheckInCollectedBy") - documentsUploaded ResidentDocument[] @relation("DocumentUploadedBy") - complaintsAnswered Complaint[] @relation("ComplaintRespondedBy") - // Login credentials (email + password) live on Account, never here. - account Account? - - @@index([code]) - @@index([role]) -} - -// ============================================================================= -// ACCOUNTS (email + password login, verification, password reset) -// ============================================================================= - -/// One human's login. The CODE is the identity (staff User, resident -/// Resident); the ACCOUNT is the credentials on top of it — and one human may -/// hold BOTH roles: a caseworker who also lives in a shared flat, or the -/// operator of a real WG who is its admin and one of its flatmates. Putting -/// email+password on each identity row forced them to pick one and guaranteed -/// two hashes for one person; here there is exactly one of each. -/// -/// A code is claimed by registering with it. Claiming a second code from the -/// same email links it to this account (proved by the account password), which -/// is how one login carries both roles. -model Account { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - email String @unique - passwordHash String? // bcrypt; null = known email, no password set yet - emailVerifiedAt DateTime? - - // The identities this login carries. At least one is always set. - userId String? @unique - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - - residentId String? @unique - resident Resident? @relation(fields: [residentId], references: [id], onDelete: Cascade) - - authTokens AuthToken[] - - @@index([userId]) - @@index([residentId]) -} - -enum AuthTokenPurpose { - VERIFY_EMAIL - RESET_PASSWORD -} - -/// Single-use, expiring token for email flows. Belongs to the ACCOUNT, not to -/// an identity: a reset proves control of the mailbox, which is exactly what -/// an account is. Only the SHA-256 hash is stored; the raw token exists once, -/// in the email. -model AuthToken { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - tokenHash String @unique - purpose AuthTokenPurpose - expiresAt DateTime - usedAt DateTime? - - accountId String - account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) - - @@index([accountId, purpose]) -} - -/// How wide a staff member's view is. Deliberately NOT part of StaffRole: -/// role answers "what work do I do", this answers "whose files may I open", -/// and one person can vary either independently of the other. -enum StaffScope { - /// Only the care domain their role is staffed for. - OWN_DOMAIN - /// Every domain, every client. - ALL_DOMAINS -} - -/// The care domain a staff member is staffed for. ONE fact per value — each -/// maps 1:1 onto a CareRole, and that bijection is derived, never restated. -/// -/// The house this runs for has three people: a Betreuerin who also sees -/// everything (BETREUUNG + ALL_DOMAINS), a job coach (JOBCOACH), and a -/// volunteering coordinator (FREIWILLIGENARBEIT). -/// -/// This used to add "There is no Leitung." — false, corrected 2026-08-31. AOZ -/// was recruiting a Programmleiter*in and a Teamleiter*in Betreuung for the -/// pilot this product is named after. A Teamleiter*in needs no new enum value: -/// it is BETREUUNG + ALL_DOMAINS + not isSystemAdmin. Adding LEITUNG here would -/// rebuild the bundled role that ADMIN was retired for. -enum StaffRole { - /// DEPRECATED. Retained ONLY so live JWTs and existing rows keep resolving; - /// no new account may be created with it. What it used to mean is now two - /// separate facts — `User.scope` and `User.isSystemAdmin` — because "sees - /// everything" and "can reconfigure the product" are different jobs. - ADMIN - BETREUUNG // Housing and daily living: placements, incidents, maintenance - SOZIALARBEIT // People and learning — no housing writes - JOBCOACH // Work, training and the documents that go with them - FREIWILLIGENARBEIT // Volunteering, the marketplace and house events -} - -model AuditLog { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // What happened - action String - entity String - entityId String - - // Who did it (optional during pilot, required after auth) - userId String? - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - // Details - changes Json? - reason String? - - @@index([entity, entityId]) - @@index([createdAt]) - @@index([userId]) -} - -// ============================================================================= -// RESIDENT ACTIVITIES -// ============================================================================= - -model Activity { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Content - title String - description String - category ActivityCategory - cost ActivityCost @default(FREE) - costNote String? - - // Practical details - location String? - website String? - phone String? - schedule String? - startsAt DateTime? - endsAt DateTime? - - // Publishing - status ActivityStatus @default(DRAFT) - highlight Boolean @default(false) - - // Ownership - createdBy User? @relation("ActivityCreatedBy", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdByUserId String? - updatedBy User? @relation("ActivityUpdatedBy", fields: [updatedByUserId], references: [id], onDelete: SetNull) - updatedByUserId String? - - @@index([status, category]) - @@index([status, highlight]) - @@index([endsAt]) -} - -enum ActivityCategory { - SPORT - LANGUAGE - CULTURE - COMMUNITY - FAMILY - SUPPORT -} - -enum ActivityCost { - FREE - REDUCED - PAID -} - -enum ActivityStatus { - DRAFT - PUBLISHED - ARCHIVED -} - -// ============================================================================= -// HOUSE EVENTS — internal, RSVP'd events (house meetings, social gatherings). -// ============================================================================= -// Distinct from Activity: Activity is a curated, admin-authored listing of -// EXTERNAL offerings with no attendee state. A HouseEvent is something -// happening inside the unit itself — staff or a resident calls it, roommates -// RSVP. Keeping the two models separate keeps that trust distinction visible -// in the portal instead of blurring "AOZ-curated offer" with "the house is -// throwing a thing". - -model HouseEvent { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - - title String - description String - category HouseEventCategory @default(SOCIAL) - location String? - startsAt DateTime - endsAt DateTime? - - status HouseEventStatus @default(PUBLISHED) - - // Creator (dual: staff OR resident — mirrors HouseholdTask's creator duality) - createdByStaff User? @relation("HouseEventCreatedByStaff", fields: [createdByStaffId], references: [id], onDelete: SetNull) - createdByStaffId String? - createdByResident Resident? @relation("HouseEventCreatedByResident", fields: [createdByResidentId], references: [id], onDelete: SetNull) - createdByResidentId String? - - rsvps EventRsvp[] - - @@index([housingUnitId, startsAt]) - @@index([status, startsAt]) -} - -enum HouseEventCategory { - HOUSE_MEETING - SOCIAL - CULTURE - SUPPORT -} - -enum HouseEventStatus { - DRAFT - PUBLISHED - CANCELLED -} - -model EventRsvp { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - event HouseEvent @relation(fields: [eventId], references: [id], onDelete: Cascade) - eventId String - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - status EventRsvpStatus @default(GOING) - - @@unique([eventId, residentId]) - @@index([eventId]) -} - -enum EventRsvpStatus { - GOING - MAYBE - DECLINED -} - -// ============================================================================= -// MAINTENANCE REQUESTS -// ============================================================================= - -model MaintenanceRequest { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Location - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - spot PlacementSpot? @relation(fields: [spotId], references: [id], onDelete: SetNull) - spotId String? - - // Request details - category MaintenanceCategory - priority MaintenancePriority @default(NORMAL) - title String - description String - location String? // Specific location within unit (e.g., "Bathroom", "Kitchen") - - // Who reported it - reportedBy Resident? @relation(fields: [reportedById], references: [id], onDelete: SetNull) - reportedById String? - reporterName String? // For non-resident reporters - - // Assignment - assignedTo String? // Staff member name - assignedAt DateTime? - - // Status tracking - status MaintenanceStatus @default(OPEN) - startedAt DateTime? - completedAt DateTime? - - // Resolution - resolution String? - cost Float? - notes String? - - @@index([housingUnitId]) - @@index([status]) - @@index([priority, status]) - @@index([createdAt]) - @@index([reportedById]) -} - -enum MaintenanceCategory { - PLUMBING // Water, drains, toilets - ELECTRICAL // Power, lights, outlets - HEATING_COOLING // Heating, AC, ventilation - APPLIANCE // Refrigerator, stove, washer - STRUCTURAL // Walls, floors, ceilings, doors - PEST_CONTROL // Insects, rodents - SECURITY // Locks, alarms, cameras - CLEANING // Deep cleaning, sanitation - EXTERIOR // Garden, parking, common areas - OTHER // Miscellaneous -} - -enum MaintenancePriority { - LOW // Can wait a week - NORMAL // Should be done within a few days - HIGH // Should be done within 24 hours - URGENT // Same-day emergency -} - -enum MaintenanceStatus { - OPEN // New request - ASSIGNED // Assigned to maintenance staff - IN_PROGRESS // Work has started - ON_HOLD // Waiting for parts/access - COMPLETED // Work finished - CANCELLED // Request cancelled -} - -// ============================================================================= -// HOUSEHOLD TASKS (Chore Management) -// ============================================================================= - -model HouseholdTask { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Location - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - - // Details - title String - description String? - instructions String? - - // Definition of done: an ordered list of binary, observable ACTIONS - // ("Lavabo gespült"), never an outcome ("Bad ist sauber"). Outcomes have a - // subjective quality gradient, so two people with different standards can - // both be honestly right about whether one was met; actions have no gradient. - // This is what makes "erledigt" checkable without anyone agreeing on what - // "sauber" means. Empty = no agreed standard yet. - checklist String[] @default([]) - - // Ordered rotation of resident ids for RECURRING_SCHEDULED tasks. Whose turn - // it is, is DERIVED from this list plus the completion count (see - // lib/chores/rotation.ts) — never stored, so it cannot drift from the log. - // A turn is a default responsibility, not a lock: anyone may complete any - // task at any time, and the balance credits whoever actually did it. - rotationResidentIds String[] @default([]) - - // Classification - taskType HouseholdTaskType @default(ONE_TIME) - category HouseholdTaskCategory @default(OTHER) - priority HouseholdTaskPriority @default(NORMAL) - - // Schedule (for recurring) - scheduleHuman String? // Human-readable, e.g., "Jeden Montag" - estimatedMinutes Int? - - // Status - currentStatus HouseholdTaskStatus @default(IDLE) - isCompleted Boolean @default(false) - completedAt DateTime? - - // Creator (dual: resident OR staff) - createdByResident Resident? @relation("TaskCreator", fields: [createdByResidentId], references: [id], onDelete: SetNull) - createdByResidentId String? - createdByStaff String? // Staff name (no FK, staff auth is separate) - - // Relations - completions TaskCompletion[] - attentionFlags TaskAttentionFlag[] - requests TaskRequest[] - - @@index([housingUnitId, currentStatus]) - @@index([housingUnitId, category]) -} - -model TaskCompletion { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // What was completed - task HouseholdTask @relation(fields: [taskId], references: [id], onDelete: Cascade) - taskId String - - // Who completed it - completedBy Resident @relation(fields: [completedById], references: [id], onDelete: Cascade) - completedById String - - // Details - completedAt DateTime @default(now()) - notes String? - durationMinutes Int? - - // Which checklist items were actually ticked, by label. Stored as labels - // rather than indices so a later amendment of the task's checklist cannot - // silently rewrite what a past completion claimed to have done. - completedItems String[] @default([]) - - // Reverse relations - resolvedFlags TaskAttentionFlag[] @relation("FlagResolvedByCompletion") - fulfilledRequests TaskRequest[] @relation("RequestFulfilledByCompletion") - - @@index([taskId]) - @@index([completedById]) -} - -model TaskAttentionFlag { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // What needs attention - task HouseholdTask @relation(fields: [taskId], references: [id], onDelete: Cascade) - taskId String - - // Who flagged it - flaggedBy Resident @relation(fields: [flaggedById], references: [id], onDelete: Cascade) - flaggedById String - - // Details - message String? - - // Resolution - isResolved Boolean @default(false) - resolvedAt DateTime? - resolvedByCompletion TaskCompletion? @relation("FlagResolvedByCompletion", fields: [resolvedByCompletionId], references: [id]) - resolvedByCompletionId String? - - @@index([taskId]) -} - -model TaskRequest { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - // What task - task HouseholdTask @relation(fields: [taskId], references: [id], onDelete: Cascade) - taskId String - - // Who requested - requestedBy Resident @relation("TaskRequestsMade", fields: [requestedById], references: [id], onDelete: Cascade) - requestedById String - - // Who is requested (null = broadcast to all) - requestedResident Resident? @relation("TaskRequestsReceived", fields: [requestedResidentId], references: [id], onDelete: SetNull) - requestedResidentId String? - isBroadcast Boolean @default(false) - - // Details - message String? - status TaskRequestStatus @default(PENDING) - responseMessage String? - - // Fulfillment - completion TaskCompletion? @relation("RequestFulfilledByCompletion", fields: [completionId], references: [id]) - completionId String? - - @@index([taskId]) - @@index([requestedResidentId]) -} - -enum HouseholdTaskType { - ONE_TIME // Do once - RECURRING_SCHEDULED // Regular schedule (e.g., weekly) - RECURRING_AS_NEEDED // Do when needed (e.g., take out trash) -} - -enum HouseholdTaskCategory { - CLEANING - SHOPPING - MAINTENANCE - COOKING - TRASH - OTHER -} - -enum HouseholdTaskPriority { - LOW - NORMAL - HIGH - URGENT -} - -enum HouseholdTaskStatus { - IDLE // No action needed - NEEDS_ATTENTION // Flagged by someone - REQUESTED // Someone requested help - IN_PROGRESS // Being worked on -} - -enum TaskRequestStatus { - PENDING - ACCEPTED - DECLINED - COMPLETED -} - -// ============================================================================= -// MARKETPLACE — resident-to-resident give away / lend / ask for -// ============================================================================= -// Per-unit scoped like HouseholdTask; resident FKs are Cascade/SetNull, not -// Restrict — unlike expense payers, no financial state depends on who posted -// or claimed a listing. - -model MarketplacePost { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - - postedBy Resident @relation("MarketplacePostedBy", fields: [postedById], references: [id], onDelete: Cascade) - postedById String - - title String - description String - kind MarketplacePostKind - /// A category id from `lib/config/marketplace.ts`, NOT a database enum. - /// Vocabulary belongs in config — adding "Fahrrad" must be one line, never a - /// migration — the same rule the shared-expense categories follow. `kind` - /// stays an enum next door because it is behaviour, not vocabulary: it - /// decides who claims from whom, so a new one means new code anyway. - category String @default("OTHER") - - /// How to reach the poster, in their own words ("Zimmer 3, abends"). - /// - /// The product has no resident-to-resident messaging — a thread belongs to - /// one resident and the other side is always staff. So without this the - /// board could match two people and then strand them: claimed, and no way to - /// arrange the handover. Shown ONLY to the person who claimed and to staff, - /// never on the open listing, because a phone number readable by every - /// resident of every unit is not what someone means when they write one. - contactNote String? - - status MarketplacePostStatus @default(OPEN) - claimedBy Resident? @relation("MarketplacePostClaimedBy", fields: [claimedById], references: [id], onDelete: SetNull) - claimedById String? - claimedAt DateTime? - closedAt DateTime? - - // Staff moderation - hiddenByStaff Boolean @default(false) - hiddenReason String? - - @@index([housingUnitId, status]) - @@index([postedById]) -} - -/// What is being exchanged, and in which direction. -/// -/// The first three move an OBJECT; the last two move somebody's TIME. Both -/// halves matter: the thing people in a shared house pass around most is not a -/// toaster but half an hour — translating a letter, watching a child, carrying -/// a wardrobe — and none of it was recorded anywhere, so the one person who -/// could help never heard that anyone needed it. -/// -/// Direction is why this is an enum and not a label: on a GIVE_AWAY the poster -/// holds the thing and the other person takes it; on a WANTED or a NEED_HELP -/// the poster is the one asking, and answering means offering. The button has -/// to say different words, so the code has to branch, so the database may as -/// well refuse a value the code cannot handle. -/// -/// There is deliberately no price anywhere near this model. @see -/// lib/config/marketplace.ts for why paid work must stay in `Opportunity`. -enum MarketplacePostKind { - GIVE_AWAY - LEND - WANTED - OFFER_HELP - NEED_HELP -} - -enum MarketplacePostStatus { - OPEN - CLAIMED - CLOSED -} - -// ============================================================================= -// TRANSFER REQUESTS (Resident-initiated) -// ============================================================================= - -model TransferRequest { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Who is requesting - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - // Current placement context - currentPlacement Placement? @relation("TransferFromPlacement", fields: [currentPlacementId], references: [id], onDelete: SetNull) - currentPlacementId String? - - // Optional target - targetUnit HousingUnit? @relation(fields: [targetUnitId], references: [id], onDelete: SetNull) - targetUnitId String? - - // Request details - reason String - status TransferRequestStatus @default(PENDING) - - // Staff review - staffNotes String? - reviewedBy String? - reviewedAt DateTime? - - @@index([residentId]) - @@index([status]) -} - -enum TransferRequestStatus { - PENDING - APPROVED - DENIED - COMPLETED - CANCELLED -} - -// Singleton config row for pilot baseline measurements and org settings. -// id is always "singleton" — upsert guarantees exactly one row. -model SystemConfig { - id String @id @default("singleton") - updatedAt DateTime @updatedAt - - // Pre-system baseline metrics (entered manually by admin from Phase-1 tracking) - pilotBaselineIncidentsPerMonth Float? - pilotBaselineRelocationsPerMonth Float? - pilotBaselineMediationHoursPerWeek Float? - pilotStartDate DateTime? -} - -// ============================================================================= -// HOUSE RULES (two tiers: AOZ-wide floor + rules a house sets for itself) -// ============================================================================= - -// One table, two scopes. An ORG rule states the AOZ floor and declares how much -// room a house has on that topic (`delegation`). A UNIT rule always points at -// the ORG rule it specialises via `parentRule`, so every house rule is -// traceable to the AOZ topic that permits it — and a house can never legislate -// on a FIXED topic. -model HouseRule { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - scope RuleScope - - // Null for ORG rules, always set for UNIT rules. - housingUnit HousingUnit? @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String? - - // Stable identifier for ORG rules (from ORG_RULE_CATALOG). Code and tests - // reference rules by key, never by title text. Null for UNIT rules. - key String? @unique - - category RuleCategory - title String - body String - - // Only meaningful on ORG rules. - delegation RuleDelegation @default(FIXED) - - parentRule HouseRule? @relation("RuleSpecialisation", fields: [parentRuleId], references: [id], onDelete: Restrict) - parentRuleId String? - childRules HouseRule[] @relation("RuleSpecialisation") - - status RuleStatus @default(ACTIVE) - - // Bumped on every content change. Acknowledgements are per-version, so an - // amended rule must be re-acknowledged by everyone it binds — nobody is held - // to a rule they were never shown. - version Int @default(1) - - effectiveFrom DateTime @default(now()) - effectiveUntil DateTime? - - // Provenance — a house rule exists because a decision adopted it. - adoptedByProposal Proposal? @relation("ProposalAdoptedRule", fields: [adoptedByProposalId], references: [id], onDelete: SetNull) - adoptedByProposalId String? - createdByStaff String? - - acknowledgements RuleAcknowledgement[] - targetedBy Proposal[] @relation("ProposalTargetRule") - topicProposals Proposal[] @relation("ProposalTopicRule") - - @@index([scope, status]) - @@index([housingUnitId, status]) - @@index([category]) - @@index([parentRuleId]) -} - -enum RuleScope { - ORG // AOZ-wide - UNIT // Set by the residents of one housing unit -} - -enum RuleDelegation { - FIXED // Non-negotiable; no unit rule may attach - UNIT_MAY_STRENGTHEN // AOZ rule is the minimum; unit may go stricter (staff confirms) - UNIT_DECIDES // AOZ names the topic only; the house sets the norm -} - -enum RuleStatus { - ACTIVE - SUPERSEDED // Replaced by a newer rule - ARCHIVED // Repealed -} - -enum RuleCategory { - SAFETY - RESPECT - NOISE - CLEANLINESS - KITCHEN - BATHROOM - GUESTS - SHARED_SPACES - COSTS - COMMUNICATION - OTHER -} - -// A rule binds someone only once they have seen it, in a version they saw. -// The single largest cause of "rule conflicts" is a norm nobody was told about. -model RuleAcknowledgement { - id String @id @default(cuid()) - - rule HouseRule @relation(fields: [ruleId], references: [id], onDelete: Cascade) - ruleId String - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - ruleVersion Int - acknowledgedAt DateTime @default(now()) - - @@unique([ruleId, residentId, ruleVersion]) - @@index([residentId]) -} - -// ============================================================================= -// DECISIONS (how a house agrees on its own rules) -// ============================================================================= - -model Proposal { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - - type ProposalType - category RuleCategory - title String - body String - - // For AMEND / REPEAL: the unit rule being changed. - targetRule HouseRule? @relation("ProposalTargetRule", fields: [targetRuleId], references: [id], onDelete: Cascade) - targetRuleId String? - - // For ADD_RULE: the AOZ topic this house rule specialises. - parentOrgRule HouseRule? @relation("ProposalTopicRule", fields: [parentOrgRuleId], references: [id], onDelete: Restrict) - parentOrgRuleId String? - - proposedByResident Resident? @relation("ProposalAuthor", fields: [proposedByResidentId], references: [id], onDelete: SetNull) - proposedByResidentId String? - proposedByStaff String? - - status ProposalStatus @default(DISCUSSION) - - // Snapshot of the decision policy in force when voting opened. Stored rather - // than derived so a past decision stays explainable after the policy changes. - decisionMode DecisionMode - threshold VoteThreshold - quorumPercent Int - approvalPercent Int - eligibleVoterCount Int @default(0) - - discussionEndsAt DateTime? - votingOpenedAt DateTime? - votingEndsAt DateTime? - decidedAt DateTime? - - // Plain-language record of how the result was reached. No black-box outcomes. - outcomeSummary String? - - // Staff confirmation — required for advisory decisions and for anything that - // claims to strengthen an AOZ rule. - staffDecision StaffDecision? - staffNotes String? - staffUserId String? - staffDecidedAt DateTime? - - votes Vote[] - adoptedRules HouseRule[] @relation("ProposalAdoptedRule") - agreement ConflictAgreement? @relation("AgreementRuleProposal") - - @@index([housingUnitId, status]) - @@index([status, votingEndsAt]) -} - -model Vote { - id String @id @default(cuid()) - - proposal Proposal @relation(fields: [proposalId], references: [id], onDelete: Cascade) - proposalId String - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - choice VoteChoice - // Required for BLOCK — a veto must say why, so it can be mediated. - reason String? - - castAt DateTime @default(now()) - - @@unique([proposalId, residentId]) - @@index([residentId]) -} - -enum ProposalType { - ADD_RULE - AMEND_RULE - REPEAL_RULE - HOUSE_DECISION // A one-off decision that does not become a standing rule -} - -enum ProposalStatus { - DISCUSSION // Open for comment, voting not yet started - VOTING - NEEDS_STAFF_CONFIRMATION // Residents agreed; staff must confirm - ACCEPTED - REJECTED - WITHDRAWN - VETOED // Staff rejected (conflicts with an AOZ rule) - EXPIRED // Voting window closed without quorum -} - -enum DecisionMode { - RESIDENT_BINDING // Residents decide; the result stands - RESIDENT_ADVISORY // Residents decide; staff must confirm - STAFF_ONLY // Residents may propose and comment; staff decides -} - -enum VoteThreshold { - CONSENSUS // Everyone who votes agrees; a single block stops it - SUPERMAJORITY - SIMPLE_MAJORITY -} - -enum VoteChoice { - YES - NO - ABSTAIN - BLOCK // "I cannot live with this" — consensus-breaking, must give a reason -} - -enum StaffDecision { - CONFIRMED - VETOED -} - -// ============================================================================= -// CONFLICT RESOLUTION -// ============================================================================= - -enum ResolutionStage { - REPORTED // Logged, no step taken yet - SELF_RESOLUTION // The people involved try to sort it out directly - PEER_MEDIATION // Other residents / house meeting help - STAFF_MEDIATION // Caseworker mediates - FORMAL_MEASURE // Transfer, written warning, authority involvement - CLOSED -} - -// The output of a resolution step: a concrete, checkable commitment with a -// review date. Free-text "resolution" notes cannot be followed up on; an -// agreement can — and one that holds is the natural seed for a house rule. -model ConflictAgreement { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade) - incidentId String - - // What was agreed, in the words of the people involved. - terms String - - // Who helped reach it (caseworker name, or a resident acting as peer mediator). - mediatorName String? - - reviewDate DateTime - status AgreementStatus @default(PROPOSED) - outcomeNotes String? - reviewedAt DateTime? - - parties AgreementParty[] - - // The feedback loop: an agreement that held can be proposed as a house rule, - // so the house learns from the conflict instead of repeating it. - ruleProposal Proposal? @relation("AgreementRuleProposal", fields: [ruleProposalId], references: [id], onDelete: SetNull) - ruleProposalId String? @unique - - @@index([incidentId]) - @@index([status, reviewDate]) -} - -model AgreementParty { - id String @id @default(cuid()) - - agreement ConflictAgreement @relation(fields: [agreementId], references: [id], onDelete: Cascade) - agreementId String - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - acceptedAt DateTime? - declinedAt DateTime? - - @@unique([agreementId, residentId]) - @@index([residentId]) -} - -enum AgreementStatus { - PROPOSED // Drafted, not everyone has accepted yet - ACCEPTED // All parties accepted; running until the review date - HELD // Reviewed and it worked - BROKEN // Reviewed and it did not hold — escalate - EXPIRED // Review date passed with no review -} - -// ============================================================================= -// LEARNING (language tests, courses, informal learning) -// ============================================================================= -// Functional credentials only — a CEFR level is a communication fact, not a -// diagnosis. Residents can add their own records; staff (Sozialarbeit, -// Jobcoach) read and add them. Never a grade of the person. - -model LearningRecord { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - kind LearningKind - title String - status LearningStatus @default(PLANNED) - - // Language tests (CEFR). languageCode is ISO 639-1 uppercased (DE, EN, FR). - languageCode String? - cefrLevel String? // A1 A2 B1 B2 C1 C2 - - provider String? // e.g. AOZ, EB Zürich, fide - category String? // language, integration, vocational, digital, other - hours Int? - startedAt DateTime? - completedAt DateTime? - notes String? - - // Who recorded it — resident self-report vs staff. Not a permission bit. - recordedBy ResidentOrStaff - - // Set when this record was generated by an application that actually - // started, rather than typed in by hand. - fromApplication OpportunityApplication? - - @@index([residentId, kind]) - @@index([status]) - @@index([languageCode, cefrLevel]) -} - -enum LearningKind { - LANGUAGE_TEST - COURSE - INFORMAL - QUALIFICATION - VOLUNTEERING - COMMUNITY_SERVICE - // Kept in step with OpportunityKind: a started application becomes a - // LearningRecord of the SAME kind with no translation table, so every - // OpportunityKind must exist here too. Pinned by opportunity-kinds.test.ts. - EMPLOYMENT - INTERNSHIP -} - -enum LearningStatus { - PLANNED - IN_PROGRESS - COMPLETED - EXPIRED -} - -enum ResidentOrStaff { - RESIDENT - STAFF -} - -// ============================================================================= -// CARE TEAM — who is responsible for this resident -// ============================================================================= -// One named person per role. Staff assign from the resident file; the resident -// sees the same names in the portal. Not a second org chart — four seats that -// match how AOZ actually splits the work. - -enum CareRole { - HOUSING - SOCIAL - JOB - VOLUNTEERING -} - -/// Which units a staff member covers, when they do not cover all of them. -/// -/// A join rather than a `String[]` of ids on User: a unit that closes must take -/// its access rows with it, and a site portfolio that turns over every few -/// years would otherwise accumulate ids pointing at nothing. Cascade on the -/// unit, Cascade on the user — this table is pure access wiring and carries no -/// history worth keeping once either side is gone. -model StaffUnit { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - - staff User @relation(fields: [staffId], references: [id], onDelete: Cascade) - staffId String - housingUnit HousingUnit @relation(fields: [housingUnitId], references: [id], onDelete: Cascade) - housingUnitId String - - @@unique([staffId, housingUnitId]) - @@index([housingUnitId]) -} - -/// How wide a staff member's SITE reach is. Orthogonal to `StaffScope`, which -/// is about care domains — a Jobcoach with ALL_DOMAINS still works one seat, -/// and a Betreuer with ALL_UNITS still works one domain. -enum SiteAccess { - /// Every unit in the product. The default, and what everyone had before - /// this axis existed. - ALL_UNITS - /// Only the units joined through StaffUnit. - ASSIGNED_UNITS -} - -model CareAssignment { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - staff User @relation(fields: [staffId], references: [id], onDelete: Restrict) - staffId String - role CareRole - - @@unique([residentId, role]) - @@index([staffId]) -} - -enum AppointmentStatus { - /// A resident asked for a meeting and nobody has answered yet. First in the - /// list because it is where an appointment now begins when the resident - /// starts it — staff-created ones still open at SCHEDULED. - REQUESTED - SCHEDULED - COMPLETED - CANCELLED - NO_SHOW -} - -model Appointment { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - - /// Null while a REQUESTED appointment is still unclaimed. - /// - /// A resident asking for a meeting does not know who will take it, and on a - /// deployment where nobody holds the care seat yet there is no one to name. - /// Requiring a staff member here would have made the request feature dead on - /// arrival for exactly the residents who need it — the same deadlock that - /// killed caseload scoping. Null means "nobody has picked this up". - staff User? @relation(fields: [staffId], references: [id], onDelete: Restrict) - staffId String? - - domain CareRole - - title String - startsAt DateTime - endsAt DateTime? - location String? - notes String? - status AppointmentStatus @default(SCHEDULED) - - /// What the resident asked for, in their words, when they requested it. - /// Null on an appointment staff scheduled themselves. - residentNote String? - - /// The answer the resident reads: why a time moved, or why a request was - /// declined. Same rule as TransferRequest.staffNotes — a decision stored and - /// never rendered is the same as no decision. - staffNote String? - - /// Set when staff recorded how the person was doing while closing this - /// appointment. Absent on an appointment nobody was asked in. - checkIn SatisfactionCheckIn? - - @@index([residentId, startsAt]) - @@index([staffId, startsAt]) - @@index([status]) - @@index([status, domain]) -} - -// Domain-scoped facts for staff work. Keys come from CARE_ATTRIBUTE_CATALOG — -// adding a field is a catalog line, never a column. No diagnoses, no asylum. -model CareAttribute { - id String @id @default(cuid()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - domain CareRole - key String - value String - updatedBy User @relation(fields: [updatedById], references: [id], onDelete: Restrict) - updatedById String - - @@unique([residentId, domain, key]) - @@index([residentId, domain]) -} - -// ============================================================================= -// OPPORTUNITIES — places a resident could go next (volunteering, community -// service), and the pipeline of who is going there. -// -// LearningRecord already answers "what has this person done". Nothing answered -// "what could they do", so every lead lived in a coach's spreadsheet and every -// application thread was lost the moment it left the conversation. -// -// The eligibility rule is the load-bearing design decision: the REQUIREMENT is -// declared by the OPPORTUNITY, never by the person. A board that filtered on -// permit status would need to store permit status, and this product does not -// record immigration status — so each listing states what it needs, and the -// coach (who knows the case) does the matching. Zero new personal data, and no -// resident is shown a place they cannot legally take. -// ============================================================================= - -model Opportunity { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Values are a subset of LearningKind on purpose: a started application - // becomes a LearningRecord of the same kind, and that mapping must not need - // a translation table. Pinned by opportunity-kinds-are-learning-kinds.test.ts. - kind OpportunityKind - title String - description String - - // The organisation offering the place. The relationship with them is the - // scarce asset a coach actually manages, so it is a field, not free text - // buried in the description. - organisation String - - location String? - schedule String? - hoursPerWeek Int? - seats Int? - - // What the PLACE requires. Never what the person is. - germanLevel String? // CEFR A1..C2; null = no level stated - permitRequirement PermitRequirement @default(NONE) - requirementNote String? - - contactName String? - contactEmail String? - contactPhone String? - website String? - - status OpportunityStatus @default(DRAFT) - startsAt DateTime? - endsAt DateTime? - - createdBy User? @relation("OpportunityCreatedBy", fields: [createdByUserId], references: [id], onDelete: SetNull) - createdByUserId String? - updatedBy User? @relation("OpportunityUpdatedBy", fields: [updatedByUserId], references: [id], onDelete: SetNull) - updatedByUserId String? - - applications OpportunityApplication[] - - @@index([status, kind]) - @@index([endsAt]) -} - -// One resident's thread with one opportunity. The pipeline — not the listing — -// is what makes this a tool rather than a noticeboard: a coach's real question -// is "where is everyone", and a directory alone cannot answer it. -model OpportunityApplication { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - resident Resident @relation(fields: [residentId], references: [id], onDelete: Cascade) - residentId String - // Restrict, not Cascade: applications are the record of what someone did. - // Listings are archived when they end, never deleted out from under them. - opportunity Opportunity @relation(fields: [opportunityId], references: [id], onDelete: Restrict) - opportunityId String - - stage ApplicationStage @default(INTERESTED) - stageChangedAt DateTime @default(now()) - note String? - - // Whether the resident put themselves forward or staff did it for them. - // Same distinction as LearningRecord.recordedBy, and not a permission bit. - createdBy ResidentOrStaff - - // The coach walking this one. Restrict so losing a staff row cannot silently - // orphan the thread; reassignment is an edit, not a deletion. - supportedBy User? @relation("ApplicationSupportedBy", fields: [supportedByUserId], references: [id], onDelete: SetNull) - supportedByUserId String? - - // Set once the placement actually STARTED. Unique, so the evidence can be - // generated exactly once no matter how often the stage is corrected. - learningRecord LearningRecord? @relation(fields: [learningRecordId], references: [id], onDelete: SetNull) - learningRecordId String? @unique - - @@unique([residentId, opportunityId]) - @@index([stage]) - @@index([opportunityId, stage]) - @@index([residentId]) -} - -enum OpportunityKind { - VOLUNTEERING - COMMUNITY_SERVICE - // Work. Distinct from the two above because they are unpaid by definition - // and these are not, which changes what the law asks of the person taking - // them — see WORK_OPPORTUNITY_KINDS in lib/config/opportunities.ts and the - // authorization rule it carries. - EMPLOYMENT - INTERNSHIP -} - -// What the place needs, in the only three shapes that change what a resident -// may do. Deliberately coarse: finer detail is a case question, and case -// questions do not belong in this database. -enum PermitRequirement { - NONE - EMPLOYER_NOTIFIES - PERMIT_REQUIRED -} - -enum OpportunityStatus { - DRAFT - PUBLISHED - ARCHIVED -} - -enum ApplicationStage { - INTERESTED - APPLIED - INTERVIEW - ACCEPTED - STARTED - ENDED - DECLINED -} diff --git a/prisma/seed-demo.ts b/prisma/seed-demo.ts deleted file mode 100644 index a027aeec..00000000 --- a/prisma/seed-demo.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * CLI wrapper for the demo reset — wipe + reseed the presentation dataset. - * - * The actual logic lives in src/lib/demo/reset.ts (SSOT), shared with the - * daily reset endpoint (api/cron/reset-demo). Relative import on purpose: - * ts-node does not resolve tsconfig path aliases. - * - * Run: npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-demo.ts - */ - -import { PrismaClient } from '@prisma/client' -import { resetDemoData } from '../src/lib/demo/reset' - -const prisma = new PrismaClient() - -resetDemoData(prisma) - .then((summary) => { - console.log('✅ Demo data reset:', summary) - }) - .catch((e) => { - console.error(e) - process.exit(1) - }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/prisma/seed.ts b/prisma/seed.ts deleted file mode 100644 index c0335276..00000000 --- a/prisma/seed.ts +++ /dev/null @@ -1,2984 +0,0 @@ -/** - * Seed script for AOZ Housing - * - * Run with: npx prisma db seed - */ - -import { PrismaClient } from '@prisma/client' -import { calculateScore } from './scoring-helper' -import { syncOrgRules } from '../src/lib/governance/sync-org-rules' -import { seedIntegrationEvidence } from '../src/lib/seed/integration-evidence' -import { seedOpportunities } from '../src/lib/seed/opportunities' -import { BRAND } from '../src/lib/config/brand' - -const prisma = new PrismaClient() - -async function main() { - console.log('🌱 Seeding database...') - - // Clean existing data (order matters: FK constraints) - // Applications hold a Restrict on Opportunity, so they go first — and both - // go before residents, whose delete would otherwise be vetoed. - await prisma.opportunityApplication.deleteMany() - await prisma.opportunity.deleteMany() - await prisma.taskRequest.deleteMany() - await prisma.taskAttentionFlag.deleteMany() - await prisma.taskCompletion.deleteMany() - await prisma.householdTask.deleteMany() - await prisma.maintenanceRequest.deleteMany() - await prisma.incidentFollowUp.deleteMany() - await prisma.satisfactionCheckIn.deleteMany() - await prisma.incidentInvolvement.deleteMany() - await prisma.incident.deleteMany() - await prisma.transferRequest.deleteMany() - await prisma.compatibilityAssessment.deleteMany() - await prisma.placement.deleteMany() - await prisma.placementSpot.deleteMany() - await prisma.resident.deleteMany() - await prisma.housingUnit.deleteMany() - await prisma.algorithmWeight.deleteMany() - - // Create algorithm weights - await prisma.algorithmWeight.create({ - data: { - lifestyleWeight: 30, - socialWeight: 25, - practicalWeight: 25, - riskWeight: 20, - factorWeights: { - sleep: 40, - noise: 30, - cleanliness: 30, - socialStyle: 35, - language: 40, - privacy: 25, - smoking: 40, - sharedSpaces: 30, - pets: 15, - dietary: 15, - }, - active: true, - notes: 'Initial weights', - }, - }) - - // Create housing units - const units = await Promise.all([ - prisma.housingUnit.create({ - data: { - code: 'ZH-001', - address: 'Langstrasse 42, 8004 Zürich', - totalBeds: 4, - totalRooms: 2, - sharedRooms: 2, - privateRooms: 0, - sharedBathrooms: 1, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: false, - wheelchairAccess: false, - elevator: true, - smokingAllowed: false, - petsAllowed: false, - quietHours: '22:00-07:00', - nearPublicTransport: true, - nearHealthServices: true, - nearSchools: false, - status: 'AVAILABLE', - notes: 'Zentrale Lage, gute ÖV-Anbindung', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-002', - address: 'Badenerstrasse 120, 8004 Zürich', - totalBeds: 6, - totalRooms: 3, - sharedRooms: 2, - privateRooms: 1, - sharedBathrooms: 2, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: true, - wheelchairAccess: true, - elevator: false, - smokingAllowed: false, - petsAllowed: true, - quietHours: '22:00-07:00', - nearPublicTransport: true, - nearHealthServices: false, - nearSchools: true, - status: 'AVAILABLE', - notes: 'Erdgeschoss, barrierefrei', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-003', - address: 'Seestrasse 55, 8002 Zürich', - totalBeds: 3, - totalRooms: 3, - sharedRooms: 0, - privateRooms: 3, - sharedBathrooms: 1, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: false, - wheelchairAccess: false, - elevator: true, - smokingAllowed: false, - petsAllowed: false, - quietHours: '21:00-08:00', - nearPublicTransport: true, - nearHealthServices: true, - nearSchools: false, - status: 'AVAILABLE', - notes: 'Ruhige Lage am See, alle Einzelzimmer', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-004', - address: 'Hardstrasse 88, 8005 Zürich', - totalBeds: 8, - totalRooms: 4, - sharedRooms: 4, - privateRooms: 0, - sharedBathrooms: 2, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: false, - wheelchairAccess: false, - elevator: false, - smokingAllowed: true, - petsAllowed: false, - quietHours: '23:00-06:00', - nearPublicTransport: true, - nearHealthServices: false, - nearSchools: false, - status: 'AVAILABLE', - notes: 'Grössere Unterkunft, Rauchen auf Balkon erlaubt', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-005', - address: 'Militärstrasse 30, 8004 Zürich', - totalBeds: 2, - totalRooms: 1, - sharedRooms: 1, - privateRooms: 0, - sharedBathrooms: 1, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: true, - wheelchairAccess: false, - elevator: false, - smokingAllowed: false, - petsAllowed: false, - quietHours: '22:00-07:00', - nearPublicTransport: true, - nearHealthServices: false, - nearSchools: true, - status: 'MAINTENANCE', - notes: 'Kleine Einheit, derzeit Renovation', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-006', - address: 'Birmensdorferstrasse 65, 8004 Zürich', - totalBeds: 6, - totalRooms: 3, - sharedRooms: 3, - privateRooms: 0, - sharedBathrooms: 2, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: false, - wheelchairAccess: false, - elevator: true, - smokingAllowed: false, - petsAllowed: false, - quietHours: '22:00-07:00', - nearPublicTransport: true, - nearHealthServices: false, - nearSchools: true, - status: 'AVAILABLE', - notes: 'Grosse gemischte Unterkunft, gute Lage', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-007', - address: 'Hohlstrasse 192, 8004 Zürich', - totalBeds: 4, - totalRooms: 3, - sharedRooms: 2, - privateRooms: 0, - sharedBathrooms: 1, - privateBathrooms: 1, - sharedKitchen: true, - privateKitchen: true, - groundFloor: false, - wheelchairAccess: false, - elevator: true, - smokingAllowed: false, - petsAllowed: false, - quietHours: '22:00-07:00', - nearPublicTransport: true, - nearHealthServices: true, - nearSchools: false, - status: 'AVAILABLE', - notes: 'Studio-Option für besondere Bedürfnisse', - }, - }), - prisma.housingUnit.create({ - data: { - code: 'ZH-008', - address: 'Josefstrasse 28, 8005 Zürich', - totalBeds: 3, - totalRooms: 3, - sharedRooms: 0, - privateRooms: 3, - sharedBathrooms: 1, - privateBathrooms: 0, - sharedKitchen: true, - privateKitchen: false, - groundFloor: true, - wheelchairAccess: true, - elevator: false, - smokingAllowed: false, - petsAllowed: false, - quietHours: '21:00-08:00', - nearPublicTransport: true, - nearHealthServices: true, - nearSchools: false, - status: 'AVAILABLE', - notes: 'Barrierefrei, Erdgeschoss, alle Einzelzimmer, medizintauglich', - }, - }), - ]) - - console.log(`✅ Created ${units.length} housing units`) - - // Create placement spots for each unit - // ZH-001: 2 rooms, 4 beds total (2 beds per room) - const zh001Room1 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 12, - floor: 2, - capacity: 2, - status: 'AVAILABLE', - }, - }) - const zh001Room2 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R2', - label: 'Zimmer 2', - type: 'ROOM', - squareMeters: 10, - floor: 2, - capacity: 2, - status: 'AVAILABLE', - }, - }) - const zh001Beds = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R1-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh001Room1.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R1-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh001Room1.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R2-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh001Room2.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[0].id, - code: 'R2-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh001Room2.id, - status: 'AVAILABLE', - }, - }), - ]) - - // ZH-002: 3 rooms - 2 shared (2 beds each) + 1 private room (medical) - const zh002Room1 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 14, - floor: 0, - capacity: 2, - status: 'AVAILABLE', - }, - }) - const zh002Room2 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R2', - label: 'Zimmer 2', - type: 'ROOM', - squareMeters: 12, - floor: 0, - capacity: 2, - status: 'AVAILABLE', - }, - }) - const zh002PrivateRoom = await prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R3', - label: 'Einzelzimmer', - type: 'PRIVATE_ROOM', - squareMeters: 10, - floor: 0, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }) - const zh002Beds = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R1-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh002Room1.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R1-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh002Room1.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R2-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh002Room2.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[1].id, - code: 'R2-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh002Room2.id, - status: 'AVAILABLE', - }, - }), - ]) - - // ZH-003: 3 private rooms (all medical) - const zh003Rooms = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[2].id, - code: 'R1', - label: 'Einzelzimmer 1', - type: 'PRIVATE_ROOM', - squareMeters: 12, - floor: 3, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[2].id, - code: 'R2', - label: 'Einzelzimmer 2', - type: 'PRIVATE_ROOM', - squareMeters: 10, - floor: 3, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[2].id, - code: 'R3', - label: 'Einzelzimmer 3', - type: 'PRIVATE_ROOM', - squareMeters: 11, - floor: 3, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - ]) - - // ZH-004: 4 rooms, 8 beds total (2 beds per room) - const zh004Rooms = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 10, - floor: 1, - capacity: 2, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: 'R2', - label: 'Zimmer 2', - type: 'ROOM', - squareMeters: 10, - floor: 1, - capacity: 2, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: 'R3', - label: 'Zimmer 3', - type: 'ROOM', - squareMeters: 12, - floor: 2, - capacity: 2, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: 'R4', - label: 'Zimmer 4', - type: 'ROOM', - squareMeters: 12, - floor: 2, - capacity: 2, - status: 'AVAILABLE', - }, - }), - ]) - const zh004Beds = await Promise.all( - zh004Rooms.flatMap((room, idx) => [ - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: `R${idx + 1}-B1`, - label: 'Bett A', - type: 'BED', - parentSpotId: room.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[3].id, - code: `R${idx + 1}-B2`, - label: 'Bett B', - type: 'BED', - parentSpotId: room.id, - status: 'AVAILABLE', - }, - }), - ]), - ) - - // ZH-005: 1 room, 2 beds (in maintenance) - const zh005Room = await prisma.placementSpot.create({ - data: { - housingUnitId: units[4].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 10, - floor: 0, - capacity: 2, - status: 'MAINTENANCE', - }, - }) - const zh005Beds = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[4].id, - code: 'R1-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh005Room.id, - status: 'MAINTENANCE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[4].id, - code: 'R1-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh005Room.id, - status: 'MAINTENANCE', - }, - }), - ]) - - // ZH-006: 3 rooms, 6 beds (2 beds per room) - const zh006Rooms = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[5].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 12, - floor: 1, - capacity: 2, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[5].id, - code: 'R2', - label: 'Zimmer 2', - type: 'ROOM', - squareMeters: 11, - floor: 1, - capacity: 2, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[5].id, - code: 'R3', - label: 'Zimmer 3', - type: 'ROOM', - squareMeters: 10, - floor: 2, - capacity: 2, - status: 'AVAILABLE', - }, - }), - ]) - const zh006Beds = await Promise.all( - zh006Rooms.flatMap((room, idx) => [ - prisma.placementSpot.create({ - data: { - housingUnitId: units[5].id, - code: `R${idx + 1}-B1`, - label: 'Bett A', - type: 'BED', - parentSpotId: room.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[5].id, - code: `R${idx + 1}-B2`, - label: 'Bett B', - type: 'BED', - parentSpotId: room.id, - status: 'AVAILABLE', - }, - }), - ]), - ) - - // ZH-007: 2 rooms (2 beds, 1 bed) + 1 studio - const zh007Room1 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[6].id, - code: 'R1', - label: 'Zimmer 1', - type: 'ROOM', - squareMeters: 14, - floor: 1, - capacity: 2, - status: 'AVAILABLE', - }, - }) - const zh007Room2 = await prisma.placementSpot.create({ - data: { - housingUnitId: units[6].id, - code: 'R2', - label: 'Zimmer 2', - type: 'ROOM', - squareMeters: 10, - floor: 1, - capacity: 1, - status: 'AVAILABLE', - }, - }) - const zh007Studio = await prisma.placementSpot.create({ - data: { - housingUnitId: units[6].id, - code: 'STUDIO-A', - label: 'Studio', - type: 'STUDIO', - squareMeters: 18, - floor: 2, - capacity: 1, - hasPrivateBathroom: true, - hasPrivateKitchen: true, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }) - const zh007Beds = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[6].id, - code: 'R1-B1', - label: 'Bett A', - type: 'BED', - parentSpotId: zh007Room1.id, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[6].id, - code: 'R1-B2', - label: 'Bett B', - type: 'BED', - parentSpotId: zh007Room1.id, - status: 'AVAILABLE', - }, - }), - ]) - - // ZH-008: 3 private rooms, ground floor (accessible, medical-eligible) - const zh008Rooms = await Promise.all([ - prisma.placementSpot.create({ - data: { - housingUnitId: units[7].id, - code: 'R1', - label: 'Einzelzimmer 1', - type: 'PRIVATE_ROOM', - squareMeters: 14, - floor: 0, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[7].id, - code: 'R2', - label: 'Einzelzimmer 2', - type: 'PRIVATE_ROOM', - squareMeters: 12, - floor: 0, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - prisma.placementSpot.create({ - data: { - housingUnitId: units[7].id, - code: 'R3', - label: 'Einzelzimmer 3', - type: 'PRIVATE_ROOM', - squareMeters: 12, - floor: 0, - capacity: 1, - requiresMedicalDocs: true, - status: 'AVAILABLE', - }, - }), - ]) - - // Collect all spots for later use - const allSpots = { - zh001: { rooms: [zh001Room1, zh001Room2], beds: zh001Beds }, - zh002: { rooms: [zh002Room1, zh002Room2], privateRoom: zh002PrivateRoom, beds: zh002Beds }, - zh003: { rooms: zh003Rooms }, - zh004: { rooms: zh004Rooms, beds: zh004Beds }, - zh005: { room: zh005Room, beds: zh005Beds }, - zh006: { rooms: zh006Rooms, beds: zh006Beds }, - zh007: { rooms: [zh007Room1, zh007Room2], studio: zh007Studio, beds: zh007Beds }, - zh008: { rooms: zh008Rooms }, - } - - console.log(`✅ Created placement spots for ${units.length} units`) - - // Create residents - const residents = await Promise.all([ - // Placed residents - prisma.resident.create({ - data: { - code: 'RES-001', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['ar', 'en'], - culturalRegion: 'Middle East', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 4, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Studiert Informatik an der ETH', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-002', - ageRange: 'ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'NIGHT_OWL', - noiseTolerance: 4, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'EXTROVERTED', - languages: ['ar', 'fr'], - culturalRegion: 'Middle East', - smokingStatus: 'OUTDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 3, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Arbeitet als Koch, Spätschicht', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-003', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 2, - cleanlinessPractice: 5, - cleanlinessExpectation: 5, - chaosTolerance: 6 - 5, - socialStyle: 'INTROVERTED', - languages: ['uk', 'ru', 'en'], - culturalRegion: 'Eastern Europe', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: false, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 5, - choresContribution: 5, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'NEEDS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Ärztin, wartet auf Anerkennung', - // Medical docs for private room eligibility - hasMedicalDocumentation: true, - medicalDocType: 'PRIVATE_ROOM', - medicalDocDate: new Date(Date.now() - 100 * 24 * 60 * 60 * 1000), - medicalDocNotes: 'Benötigt Einzelzimmer aufgrund erhöhtem Privatsphärebedürfnis', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-004', - ageRange: 'MIDDLE_AGED', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['ti', 'en'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 4, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Gelernter Elektriker', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-005', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 4, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'EXTROVERTED', - languages: ['fa', 'en'], - culturalRegion: 'Central Asia', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 3, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: true, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'ELEVATED', - status: 'PLACED', - notes: 'Macht Deutschkurs B1', - // Medical docs for private room eligibility (in ZH-003) - hasMedicalDocumentation: true, - medicalDocType: 'BOTH', - medicalDocDate: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000), - medicalDocNotes: 'Psychologische Empfehlung für ruhige Umgebung', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-006', - ageRange: 'SENIOR', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 1, - cleanlinessPractice: 5, - cleanlinessExpectation: 5, - chaosTolerance: 6 - 5, - socialStyle: 'INTROVERTED', - languages: ['tr', 'de'], - culturalRegion: 'Middle East', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'GROUND_FLOOR', - medicalEquipment: true, - petTolerance: false, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 5, - choresContribution: 2, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: true, - supportLevel: 'ELEVATED', - status: 'PLACED', - notes: 'Pensioniert, braucht CPAP-Gerät nachts', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-007', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['so', 'ar', 'en'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 4, - choresContribution: 5, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'NEEDS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Arbeitet Teilzeit im Reinigungsbereich', - // Medical docs for private room eligibility (in ZH-003) - hasMedicalDocumentation: true, - medicalDocType: 'PRIVATE_ROOM', - medicalDocDate: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000), - medicalDocNotes: 'Ärztliches Attest für Einzelzimmer', - }, - }), - // Unplaced residents (waiting for placement) - prisma.resident.create({ - data: { - code: 'RES-008', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'NIGHT_OWL', - noiseTolerance: 5, - cleanlinessPractice: 2, - cleanlinessExpectation: 2, - chaosTolerance: 6 - 2, - socialStyle: 'EXTROVERTED', - languages: ['ps', 'fa'], - culturalRegion: 'Central Asia', - smokingStatus: 'INDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 1, - choresContribution: 2, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Neu angekommen, braucht Raucherunterkunft', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-009', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 2, - cleanlinessPractice: 5, - cleanlinessExpectation: 5, - chaosTolerance: 6 - 5, - socialStyle: 'INTROVERTED', - languages: ['uk', 'en'], - culturalRegion: 'Eastern Europe', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['vegetarian'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 5, - choresContribution: 5, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'IT-Fachfrau, sucht ruhige Unterkunft', - }, - }), - prisma.resident.create({ - data: { - code: 'RES-010', - ageRange: 'MIDDLE_AGED', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'IRREGULAR', - noiseTolerance: 3, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'MODERATE', - languages: ['ar', 'en', 'de'], - culturalRegion: 'Middle East', - smokingStatus: 'OUTDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 3, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Taxifahrer, unregelmässige Arbeitszeiten', - }, - }), - // --- New residents (RES-011 to RES-025) --- - // RES-011: Hard-to-place — night owl, indoor smoker - prisma.resident.create({ - data: { - code: 'RES-011', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'NIGHT_OWL', - noiseTolerance: 4, - cleanlinessPractice: 2, - cleanlinessExpectation: 2, - chaosTolerance: 6 - 2, - socialStyle: 'MODERATE', - languages: ['ps', 'fa'], - culturalRegion: 'Central Asia', - smokingStatus: 'INDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 2, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Raucht viel, schwer zu platzieren', - }, - }), - // RES-012: Easy-to-place — clean, quiet, cooperative - prisma.resident.create({ - data: { - code: 'RES-012', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 2, - cleanlinessPractice: 5, - cleanlinessExpectation: 5, - chaosTolerance: 6 - 5, - socialStyle: 'MODERATE', - languages: ['ti', 'en'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 5, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Ordentlich, kooperativ, leicht zu platzieren', - }, - }), - // RES-013: Accessibility needs — senior, ground floor, medical equipment - prisma.resident.create({ - data: { - code: 'RES-013', - ageRange: 'SENIOR', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 2, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'INTROVERTED', - languages: ['ar'], - culturalRegion: 'Middle East', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'GROUND_FLOOR', - medicalEquipment: true, - petTolerance: false, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 4, - choresContribution: 2, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: true, - supportLevel: 'ELEVATED', - status: 'ACTIVE', - notes: 'Benötigt Erdgeschoss und Platz für medizinische Geräte', - hasMedicalDocumentation: true, - medicalDocType: 'BOTH', - medicalDocDate: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000), - medicalDocNotes: 'Ärztliche Empfehlung für Erdgeschoss und Einzelzimmer', - }, - }), - // RES-014: Good match in ZH-004 — extroverted, standard - prisma.resident.create({ - data: { - code: 'RES-014', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 4, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'EXTROVERTED', - languages: ['es', 'en'], - culturalRegion: 'South America', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 3, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Fussballfan, sucht Anschluss', - }, - }), - // RES-015: Good match pair with RES-014 in ZH-004 - prisma.resident.create({ - data: { - code: 'RES-015', - ageRange: 'ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'MODERATE', - languages: ['fr', 'sw', 'en'], - culturalRegion: 'Central Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 4, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Gelernter Mechaniker, spricht drei Sprachen', - }, - }), - // RES-016: Tension pair with RES-017 — night owl, introverted - prisma.resident.create({ - data: { - code: 'RES-016', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'NIGHT_OWL', - noiseTolerance: 2, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'INTROVERTED', - languages: ['fa', 'en'], - culturalRegion: 'Middle East', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: false, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 5, - choresContribution: 4, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Studiert abends, braucht Ruhe', - }, - }), - // RES-017: Tension pair with RES-016 — early bird, extroverted - prisma.resident.create({ - data: { - code: 'RES-017', - ageRange: 'ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 4, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'EXTROVERTED', - languages: ['am', 'en'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 3, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Gesellig, kocht gerne für andere', - }, - }), - // RES-018: Stable in ZH-004 - prisma.resident.create({ - data: { - code: 'RES-018', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'MODERATE', - languages: ['so', 'ar'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 3, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Besucht Integrationskurs', - }, - }), - // RES-019: Stable in ZH-004 — speaks German, helps others - prisma.resident.create({ - data: { - code: 'RES-019', - ageRange: 'MIDDLE_AGED', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['tr', 'de'], - culturalRegion: 'Middle East', - smokingStatus: 'OUTDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 4, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Spricht gut Deutsch, hilft anderen Bewohnern', - }, - }), - // RES-020: Failed placement → transfer story - prisma.resident.create({ - data: { - code: 'RES-020', - ageRange: 'ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'NIGHT_OWL', - noiseTolerance: 4, - cleanlinessPractice: 2, - cleanlinessExpectation: 2, - chaosTolerance: 6 - 2, - socialStyle: 'EXTROVERTED', - languages: ['ar', 'en'], - culturalRegion: 'Middle East', - smokingStatus: 'OUTDOOR_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 1, - choresContribution: 2, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: true, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'ELEVATED', - status: 'TRANSFERRED', - notes: 'Versetzt nach Konflikten in ZH-004', - }, - }), - // RES-021: Recently arrived, waiting - prisma.resident.create({ - data: { - code: 'RES-021', - ageRange: 'YOUNG_ADULT', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['es', 'en'], - culturalRegion: 'South America', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 4, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Vor kurzem angekommen, wartet auf Platzierung', - }, - }), - // RES-022: Success story in ZH-001 - prisma.resident.create({ - data: { - code: 'RES-022', - ageRange: 'ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 3, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['ti', 'en', 'ar'], - culturalRegion: 'East Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 3, - choresContribution: 5, - recyclingKnowledge: 'GOOD', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - conflictStyle: 'COOPERATIVE', - status: 'PLACED', - notes: 'Zuverlässig, hilft bei Haushaltsaufgaben', - }, - }), - // RES-023: Complex needs, waiting - prisma.resident.create({ - data: { - code: 'RES-023', - ageRange: 'MIDDLE_AGED', - gender: 'FEMALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 2, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'INTROVERTED', - languages: ['fa', 'ps'], - culturalRegion: 'Central Asia', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: false, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 4, - choresContribution: 3, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: true, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'ELEVATED', - status: 'ACTIVE', - notes: 'Erhöhter Betreuungsbedarf, braucht ruhige Umgebung', - }, - }), - // RES-024: Transferred into better match (was ZH-001 → now ZH-006) - prisma.resident.create({ - data: { - code: 'RES-024', - ageRange: 'YOUNG_ADULT', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'STANDARD', - noiseTolerance: 4, - cleanlinessPractice: 3, - cleanlinessExpectation: 3, - chaosTolerance: 6 - 3, - socialStyle: 'EXTROVERTED', - languages: ['ar', 'en', 'de'], - culturalRegion: 'Middle East', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: ['halal'], - mobilityNeeds: 'NONE', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 2, - choresContribution: 3, - recyclingKnowledge: 'BASIC', - roomSharingStatus: 'CAN_SHARE', - hasNightDisturbances: false, - needsQuietEnvironment: false, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'PLACED', - notes: 'Spricht drei Sprachen, Deutschkurs A2', - }, - }), - // RES-025: Accessibility needs, waiting - prisma.resident.create({ - data: { - code: 'RES-025', - ageRange: 'SENIOR', - gender: 'MALE', - familyStatus: 'SINGLE', - sleepSchedule: 'EARLY_BIRD', - noiseTolerance: 2, - cleanlinessPractice: 4, - cleanlinessExpectation: 4, - chaosTolerance: 6 - 4, - socialStyle: 'MODERATE', - languages: ['fr', 'sw'], - culturalRegion: 'Central Africa', - smokingStatus: 'NON_SMOKER', - dietaryNeeds: [], - mobilityNeeds: 'GROUND_FLOOR', - medicalEquipment: false, - petTolerance: true, - sharedBathroom: true, - sharedKitchen: true, - privacyNeed: 4, - choresContribution: 3, - recyclingKnowledge: 'NONE', - roomSharingStatus: 'PREFERS_PRIVATE', - hasNightDisturbances: false, - needsQuietEnvironment: true, - hasSleepEquipment: false, - supportLevel: 'STANDARD', - status: 'ACTIVE', - notes: 'Benötigt Erdgeschoss, wartet auf passende Unterkunft', - }, - }), - ]) - - console.log(`✅ Created ${residents.length} residents`) - - // Integration evidence — language, qualification and volunteering records, - // derived from each resident's own profile. Without it /learning renders - // five zeroes and an empty list on a database holding 24 people, and the - // dashboard's learning pulse reports nothing to every coaching role. - // - // Care seats go to whichever staff account this database already has (the - // admin from prisma/seed-admin.ts); with none, evidence still seeds and the - // seats stay empty rather than conjuring a colleague. - const seedStaff = await prisma.user.findFirst({ - where: { active: true }, - select: { id: true }, - orderBy: { code: 'asc' }, - }) - const integration = await seedIntegrationEvidence(prisma, { - residentIds: residents.map((r) => r.id), - staffId: seedStaff?.id ?? null, - }) - console.log( - `✅ Created ${integration.records} learning records, ` + - `${integration.careAssignments} care assignments`, - ) - - const opportunities = await seedOpportunities(prisma, { - residentIds: residents.map((r) => r.id), - staffId: seedStaff?.id ?? null, - }) - console.log( - `✅ Created ${opportunities.opportunities} opportunities, ` + - `${opportunities.applications} applications, ` + - `${opportunities.evidenceRecords} generated records`, - ) - - // Create placements with CALCULATED compatibility scores - const now = new Date() - const DAY = 24 * 60 * 60 * 1000 - - // Calculate real compatibility scores between roommates - // ZH-001: RES-001 (index 0), RES-002 (index 1), RES-022 (index 21) - const score_0_1 = calculateScore(residents[0], residents[1]) - const score_0_21 = calculateScore(residents[0], residents[21]) - const score_1_21 = calculateScore(residents[1], residents[21]) - console.log(` 📊 RES-001 + RES-002 compatibility: ${score_0_1.compatibilityScore}%`) - console.log(` 📊 RES-001 + RES-022 compatibility: ${score_0_21.compatibilityScore}%`) - - // ZH-002: RES-003 (index 2) with RES-004 (index 3) and RES-006 (index 5) - const score_2_3 = calculateScore(residents[2], residents[3]) - const score_2_5 = calculateScore(residents[2], residents[5]) - const score_3_5 = calculateScore(residents[3], residents[5]) - - // ZH-003: RES-005 (index 4) and RES-007 (index 6) - const score_4_6 = calculateScore(residents[4], residents[6]) - - // ZH-004: RES-014 (13) + RES-015 (14), RES-018 (17) + RES-019 (18) - const score_13_14 = calculateScore(residents[13], residents[14]) - const score_17_18 = calculateScore(residents[17], residents[18]) - console.log(` 📊 RES-014 + RES-015 compatibility: ${score_13_14.compatibilityScore}%`) - console.log(` 📊 RES-018 + RES-019 compatibility: ${score_17_18.compatibilityScore}%`) - - // ZH-006: RES-016 (15) + RES-017 (16) — tension pair - const score_15_16 = calculateScore(residents[15], residents[16]) - console.log(` 📊 RES-016 + RES-017 compatibility: ${score_15_16.compatibilityScore}%`) - - // Ended placement scores - const score_19_17 = calculateScore(residents[19], residents[17]) // RES-020 was with RES-018 in ZH-004 - - // ========================================================================= - // ACTIVE PLACEMENTS - // ========================================================================= - - const placements = await Promise.all([ - // ZH-001: RES-001 and RES-002 (some tension), RES-022 (success story) - prisma.placement.create({ - data: { - residentId: residents[0].id, - housingUnitId: units[0].id, - spotId: allSpots.zh001.beds[0].id, // R1-B1 - startDate: new Date(now.getTime() - 60 * DAY), - compatibilityScore: score_0_1.compatibilityScore, - lifestyleScore: score_0_1.lifestyleScore, - socialScore: score_0_1.socialScore, - practicalScore: score_0_1.practicalScore, - riskScore: score_0_1.riskScore, - status: 'ACTIVE', - placementNotes: - score_0_1.concerns.length > 0 - ? score_0_1.concerns.join('. ') - : 'Kompatibilitätsprüfung durchgeführt', - }, - }), - prisma.placement.create({ - data: { - residentId: residents[1].id, - housingUnitId: units[0].id, - spotId: allSpots.zh001.beds[1].id, // R1-B2 - startDate: new Date(now.getTime() - 45 * DAY), - compatibilityScore: score_0_1.compatibilityScore, - lifestyleScore: score_0_1.lifestyleScore, - socialScore: score_0_1.socialScore, - practicalScore: score_0_1.practicalScore, - riskScore: score_0_1.riskScore, - status: 'ACTIVE', - placementNotes: - score_0_1.concerns.length > 0 - ? score_0_1.concerns.join('. ') - : 'Kompatibilitätsprüfung durchgeführt', - }, - }), - // [2] ZH-001 R2-B1: RES-022 (success story) - prisma.placement.create({ - data: { - residentId: residents[21].id, - housingUnitId: units[0].id, - spotId: allSpots.zh001.beds[2].id, // R2-B1 - startDate: new Date(now.getTime() - 25 * DAY), - compatibilityScore: score_0_21.compatibilityScore, - lifestyleScore: score_0_21.lifestyleScore, - socialScore: score_0_21.socialScore, - practicalScore: score_0_21.practicalScore, - riskScore: score_0_21.riskScore, - status: 'ACTIVE', - placementNotes: 'Gute Kompatibilität mit bestehenden Bewohnern', - }, - }), - // [3] ZH-002: RES-003 private room, [4] RES-004, [5] RES-006 shared rooms - prisma.placement.create({ - data: { - residentId: residents[2].id, - housingUnitId: units[1].id, - spotId: allSpots.zh002.privateRoom.id, - startDate: new Date(now.getTime() - 90 * DAY), - compatibilityScore: Math.round( - (score_2_3.compatibilityScore + score_2_5.compatibilityScore) / 2, - ), - lifestyleScore: Math.round((score_2_3.lifestyleScore + score_2_5.lifestyleScore) / 2), - socialScore: Math.round((score_2_3.socialScore + score_2_5.socialScore) / 2), - practicalScore: Math.round((score_2_3.practicalScore + score_2_5.practicalScore) / 2), - riskScore: Math.round((score_2_3.riskScore + score_2_5.riskScore) / 2), - status: 'ACTIVE', - placementNotes: 'Einzelzimmer wegen hohem Privatsphärebedürfnis (med. Dok.)', - }, - }), - prisma.placement.create({ - data: { - residentId: residents[3].id, - housingUnitId: units[1].id, - spotId: allSpots.zh002.beds[0].id, - startDate: new Date(now.getTime() - 75 * DAY), - compatibilityScore: score_3_5.compatibilityScore, - lifestyleScore: score_3_5.lifestyleScore, - socialScore: score_3_5.socialScore, - practicalScore: score_3_5.practicalScore, - riskScore: score_3_5.riskScore, - status: 'ACTIVE', - placementNotes: - score_3_5.strengths.length > 0 - ? score_3_5.strengths.join('. ') - : 'Kompatibilitätsprüfung durchgeführt', - }, - }), - prisma.placement.create({ - data: { - residentId: residents[5].id, - housingUnitId: units[1].id, - spotId: allSpots.zh002.beds[1].id, - startDate: new Date(now.getTime() - 120 * DAY), - compatibilityScore: score_3_5.compatibilityScore, - lifestyleScore: score_3_5.lifestyleScore, - socialScore: score_3_5.socialScore, - practicalScore: score_3_5.practicalScore, - riskScore: score_3_5.riskScore, - status: 'ACTIVE', - placementNotes: 'Erdgeschoss wegen Mobilität, CPAP-Gerät', - }, - }), - // [6] ZH-003: RES-005, [7] RES-007 (private rooms) - prisma.placement.create({ - data: { - residentId: residents[4].id, - housingUnitId: units[2].id, - spotId: allSpots.zh003.rooms[0].id, - startDate: new Date(now.getTime() - 30 * DAY), - compatibilityScore: score_4_6.compatibilityScore, - lifestyleScore: score_4_6.lifestyleScore, - socialScore: score_4_6.socialScore, - practicalScore: score_4_6.practicalScore, - riskScore: score_4_6.riskScore, - status: 'ACTIVE', - placementNotes: - score_4_6.strengths.length > 0 - ? score_4_6.strengths.join('. ') - : 'Kompatibilitätsprüfung durchgeführt', - }, - }), - prisma.placement.create({ - data: { - residentId: residents[6].id, - housingUnitId: units[2].id, - spotId: allSpots.zh003.rooms[1].id, - startDate: new Date(now.getTime() - 20 * DAY), - compatibilityScore: score_4_6.compatibilityScore, - lifestyleScore: score_4_6.lifestyleScore, - socialScore: score_4_6.socialScore, - practicalScore: score_4_6.practicalScore, - riskScore: score_4_6.riskScore, - status: 'ACTIVE', - placementNotes: - score_4_6.strengths.length > 0 - ? score_4_6.strengths.join('. ') - : 'Kompatibilitätsprüfung durchgeführt', - }, - }), - // [8] ZH-004: RES-014 + RES-015 (good match, R1) - prisma.placement.create({ - data: { - residentId: residents[13].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[0].id, // R1-B1 - startDate: new Date(now.getTime() - 35 * DAY), - compatibilityScore: score_13_14.compatibilityScore, - lifestyleScore: score_13_14.lifestyleScore, - socialScore: score_13_14.socialScore, - practicalScore: score_13_14.practicalScore, - riskScore: score_13_14.riskScore, - status: 'ACTIVE', - placementNotes: 'Gutes Match - ähnlicher Lebensstil', - }, - }), - // [9] - prisma.placement.create({ - data: { - residentId: residents[14].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[1].id, // R1-B2 - startDate: new Date(now.getTime() - 30 * DAY), - compatibilityScore: score_13_14.compatibilityScore, - lifestyleScore: score_13_14.lifestyleScore, - socialScore: score_13_14.socialScore, - practicalScore: score_13_14.practicalScore, - riskScore: score_13_14.riskScore, - status: 'ACTIVE', - placementNotes: 'Gutes Match mit RES-014', - }, - }), - // [10] ZH-004: RES-018 + RES-019 (stable, R3) - prisma.placement.create({ - data: { - residentId: residents[17].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[4].id, // R3-B1 - startDate: new Date(now.getTime() - 50 * DAY), - compatibilityScore: score_17_18.compatibilityScore, - lifestyleScore: score_17_18.lifestyleScore, - socialScore: score_17_18.socialScore, - practicalScore: score_17_18.practicalScore, - riskScore: score_17_18.riskScore, - status: 'ACTIVE', - placementNotes: 'Stabile Platzierung', - }, - }), - // [11] - prisma.placement.create({ - data: { - residentId: residents[18].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[5].id, // R3-B2 - startDate: new Date(now.getTime() - 45 * DAY), - compatibilityScore: score_17_18.compatibilityScore, - lifestyleScore: score_17_18.lifestyleScore, - socialScore: score_17_18.socialScore, - practicalScore: score_17_18.practicalScore, - riskScore: score_17_18.riskScore, - status: 'ACTIVE', - placementNotes: 'Spricht Deutsch, hilft bei Verständigung', - }, - }), - // [12] ZH-006: RES-016 + RES-017 (tension pair, R1) - prisma.placement.create({ - data: { - residentId: residents[15].id, - housingUnitId: units[5].id, - spotId: allSpots.zh006.beds[0].id, // R1-B1 - startDate: new Date(now.getTime() - 28 * DAY), - compatibilityScore: score_15_16.compatibilityScore, - lifestyleScore: score_15_16.lifestyleScore, - socialScore: score_15_16.socialScore, - practicalScore: score_15_16.practicalScore, - riskScore: score_15_16.riskScore, - status: 'ACTIVE', - placementNotes: - score_15_16.concerns.length > 0 - ? score_15_16.concerns.join('. ') - : 'Spannungspotential erkannt', - }, - }), - // [13] - prisma.placement.create({ - data: { - residentId: residents[16].id, - housingUnitId: units[5].id, - spotId: allSpots.zh006.beds[1].id, // R1-B2 - startDate: new Date(now.getTime() - 28 * DAY), - compatibilityScore: score_15_16.compatibilityScore, - lifestyleScore: score_15_16.lifestyleScore, - socialScore: score_15_16.socialScore, - practicalScore: score_15_16.practicalScore, - riskScore: score_15_16.riskScore, - status: 'ACTIVE', - placementNotes: - score_15_16.concerns.length > 0 - ? score_15_16.concerns.join('. ') - : 'Spannungspotential erkannt', - }, - }), - // [14] ZH-006: RES-024 (transferred here from ZH-001, R2) - prisma.placement.create({ - data: { - residentId: residents[23].id, - housingUnitId: units[5].id, - spotId: allSpots.zh006.beds[2].id, // R2-B1 - startDate: new Date(now.getTime() - 15 * DAY), - compatibilityScore: 72, - lifestyleScore: 78, - socialScore: 70, - practicalScore: 75, - riskScore: 15, - status: 'ACTIVE', - placementNotes: 'Versetzung aus ZH-001 zu besserem Match', - }, - }), - ]) - - // ========================================================================= - // ENDED PLACEMENTS (transfer/conflict stories) - // ========================================================================= - - const endedPlacements = await Promise.all([ - // RES-020 was in ZH-004 for 40 days, ended with CONFLICT - prisma.placement.create({ - data: { - residentId: residents[19].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[2].id, // R2-B1 - startDate: new Date(now.getTime() - 80 * DAY), - endDate: new Date(now.getTime() - 40 * DAY), - compatibilityScore: score_19_17.compatibilityScore, - lifestyleScore: score_19_17.lifestyleScore, - socialScore: score_19_17.socialScore, - practicalScore: score_19_17.practicalScore, - riskScore: score_19_17.riskScore, - status: 'ENDED', - endReason: 'CONFLICT', - satisfactionRating: 2, - placementNotes: 'Niedrige Kompatibilität, Nachteulen-Problematik', - outcomeNotes: 'Mehrere Konflikte, Versetzung notwendig', - conflictGap: 'NOISE', - wasPredictable: true, - }, - }), - // RES-024 was in ZH-001 for 20 days, ended with UPGRADE - prisma.placement.create({ - data: { - residentId: residents[23].id, - housingUnitId: units[0].id, - spotId: allSpots.zh001.beds[3].id, // R2-B2 - startDate: new Date(now.getTime() - 50 * DAY), - endDate: new Date(now.getTime() - 15 * DAY), - compatibilityScore: score_0_21.compatibilityScore, - lifestyleScore: score_0_21.lifestyleScore, - socialScore: score_0_21.socialScore, - practicalScore: score_0_21.practicalScore, - riskScore: score_0_21.riskScore, - status: 'ENDED', - endReason: 'UPGRADE', - satisfactionRating: 3, - placementNotes: 'Initiale Platzierung, besseres Match gefunden', - outcomeNotes: 'Versetzt nach ZH-006 für bessere Kompatibilität', - }, - }), - // RES-004 historical: was in ZH-004, natural exit before current placement - prisma.placement.create({ - data: { - residentId: residents[3].id, - housingUnitId: units[3].id, - spotId: allSpots.zh004.beds[6].id, // R4-B1 - startDate: new Date(now.getTime() - 200 * DAY), - endDate: new Date(now.getTime() - 100 * DAY), - compatibilityScore: 65, - lifestyleScore: 70, - socialScore: 60, - practicalScore: 68, - riskScore: 20, - status: 'ENDED', - endReason: 'NATURAL', - satisfactionRating: 4, - placementNotes: 'Erste Platzierung im System', - outcomeNotes: 'Bewohner vorübergehend aus System ausgetreten', - }, - }), - ]) - - console.log(`✅ Created ${placements.length} active + ${endedPlacements.length} ended placements`) - - // Update spot statuses to OCCUPIED for spots with active placements - await Promise.all([ - // ZH-001 - prisma.placementSpot.update({ - where: { id: allSpots.zh001.beds[0].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh001.beds[1].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh001.beds[2].id }, - data: { status: 'OCCUPIED' }, - }), - // ZH-002 - prisma.placementSpot.update({ - where: { id: allSpots.zh002.privateRoom.id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh002.beds[0].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh002.beds[1].id }, - data: { status: 'OCCUPIED' }, - }), - // ZH-003 - prisma.placementSpot.update({ - where: { id: allSpots.zh003.rooms[0].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh003.rooms[1].id }, - data: { status: 'OCCUPIED' }, - }), - // ZH-004 - prisma.placementSpot.update({ - where: { id: allSpots.zh004.beds[0].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh004.beds[1].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh004.beds[4].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh004.beds[5].id }, - data: { status: 'OCCUPIED' }, - }), - // ZH-006 - prisma.placementSpot.update({ - where: { id: allSpots.zh006.beds[0].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh006.beds[1].id }, - data: { status: 'OCCUPIED' }, - }), - prisma.placementSpot.update({ - where: { id: allSpots.zh006.beds[2].id }, - data: { status: 'OCCUPIED' }, - }), - ]) - - // ========================================================================= - // COMPATIBILITY ASSESSMENTS - // ========================================================================= - - const assessments = await Promise.all([ - // ZH-001 roommates - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[0].id, - comparedWithId: residents[1].id, - overallScore: score_0_1.compatibilityScore, - lifestyleScore: score_0_1.lifestyleScore, - socialScore: score_0_1.socialScore, - practicalScore: score_0_1.practicalScore, - riskScore: score_0_1.riskScore, - strengths: score_0_1.strengths, - concerns: score_0_1.concerns, - recommendations: score_0_1.recommendations, - }, - }), - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[0].id, - comparedWithId: residents[21].id, - overallScore: score_0_21.compatibilityScore, - lifestyleScore: score_0_21.lifestyleScore, - socialScore: score_0_21.socialScore, - practicalScore: score_0_21.practicalScore, - riskScore: score_0_21.riskScore, - strengths: score_0_21.strengths, - concerns: score_0_21.concerns, - recommendations: score_0_21.recommendations, - }, - }), - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[1].id, - comparedWithId: residents[21].id, - overallScore: score_1_21.compatibilityScore, - lifestyleScore: score_1_21.lifestyleScore, - socialScore: score_1_21.socialScore, - practicalScore: score_1_21.practicalScore, - riskScore: score_1_21.riskScore, - strengths: score_1_21.strengths, - concerns: score_1_21.concerns, - recommendations: score_1_21.recommendations, - }, - }), - // ZH-002 - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[2].id, - comparedWithId: residents[3].id, - overallScore: score_2_3.compatibilityScore, - lifestyleScore: score_2_3.lifestyleScore, - socialScore: score_2_3.socialScore, - practicalScore: score_2_3.practicalScore, - riskScore: score_2_3.riskScore, - strengths: score_2_3.strengths, - concerns: score_2_3.concerns, - recommendations: score_2_3.recommendations, - }, - }), - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[2].id, - comparedWithId: residents[5].id, - overallScore: score_2_5.compatibilityScore, - lifestyleScore: score_2_5.lifestyleScore, - socialScore: score_2_5.socialScore, - practicalScore: score_2_5.practicalScore, - riskScore: score_2_5.riskScore, - strengths: score_2_5.strengths, - concerns: score_2_5.concerns, - recommendations: score_2_5.recommendations, - }, - }), - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[3].id, - comparedWithId: residents[5].id, - overallScore: score_3_5.compatibilityScore, - lifestyleScore: score_3_5.lifestyleScore, - socialScore: score_3_5.socialScore, - practicalScore: score_3_5.practicalScore, - riskScore: score_3_5.riskScore, - strengths: score_3_5.strengths, - concerns: score_3_5.concerns, - recommendations: score_3_5.recommendations, - }, - }), - // ZH-003 - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[4].id, - comparedWithId: residents[6].id, - overallScore: score_4_6.compatibilityScore, - lifestyleScore: score_4_6.lifestyleScore, - socialScore: score_4_6.socialScore, - practicalScore: score_4_6.practicalScore, - riskScore: score_4_6.riskScore, - strengths: score_4_6.strengths, - concerns: score_4_6.concerns, - recommendations: score_4_6.recommendations, - }, - }), - // ZH-004 - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[13].id, - comparedWithId: residents[14].id, - overallScore: score_13_14.compatibilityScore, - lifestyleScore: score_13_14.lifestyleScore, - socialScore: score_13_14.socialScore, - practicalScore: score_13_14.practicalScore, - riskScore: score_13_14.riskScore, - strengths: score_13_14.strengths, - concerns: score_13_14.concerns, - recommendations: score_13_14.recommendations, - }, - }), - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[17].id, - comparedWithId: residents[18].id, - overallScore: score_17_18.compatibilityScore, - lifestyleScore: score_17_18.lifestyleScore, - socialScore: score_17_18.socialScore, - practicalScore: score_17_18.practicalScore, - riskScore: score_17_18.riskScore, - strengths: score_17_18.strengths, - concerns: score_17_18.concerns, - recommendations: score_17_18.recommendations, - }, - }), - // ZH-006 tension pair - prisma.compatibilityAssessment.create({ - data: { - residentId: residents[15].id, - comparedWithId: residents[16].id, - overallScore: score_15_16.compatibilityScore, - lifestyleScore: score_15_16.lifestyleScore, - socialScore: score_15_16.socialScore, - practicalScore: score_15_16.practicalScore, - riskScore: score_15_16.riskScore, - strengths: score_15_16.strengths, - concerns: score_15_16.concerns, - recommendations: score_15_16.recommendations, - }, - }), - ]) - - console.log(`✅ Created ${assessments.length} compatibility assessments`) - - // ========================================================================= - // SATISFACTION CHECK-INS - // ========================================================================= - - const checkIns = await Promise.all([ - // ZH-001 tension pair: Initial (low) → Regular (improving) → Recent (stable) - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[0].id, // RES-001 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 3, - roommateRelations: 2, - facilitySatisfaction: 4, - safetyFeeling: 4, - concerns: 'Mitbewohner ist laut abends', - collectedBy: 'Frau Müller', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[0].id, - checkInType: 'REGULAR', - weekNumber: 4, - overallSatisfaction: 3, - roommateRelations: 3, - facilitySatisfaction: 4, - safetyFeeling: 4, - positives: 'Kopfhörer-Regelung hilft', - collectedBy: 'Frau Müller', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[0].id, - checkInType: 'REGULAR', - weekNumber: 8, - overallSatisfaction: 4, - roommateRelations: 3, - facilitySatisfaction: 4, - safetyFeeling: 5, - positives: 'Situation hat sich stabilisiert', - collectedBy: 'Frau Müller', - }, - }), - // ZH-002: Consistently high - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[4].id, // RES-004 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 4, - roommateRelations: 4, - facilitySatisfaction: 5, - safetyFeeling: 5, - positives: 'Ruhige Umgebung, nette Mitbewohner', - collectedBy: 'Herr Schmidt', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[4].id, - checkInType: 'REGULAR', - weekNumber: 6, - overallSatisfaction: 5, - roommateRelations: 5, - facilitySatisfaction: 4, - safetyFeeling: 5, - positives: 'Fühle mich wohl hier', - collectedBy: 'Herr Schmidt', - }, - }), - // ZH-003: High satisfaction (private rooms) - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[6].id, // RES-005 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 5, - roommateRelations: 4, - facilitySatisfaction: 5, - safetyFeeling: 5, - positives: 'Eigenes Zimmer ist sehr wichtig für mich', - collectedBy: 'Frau Müller', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[7].id, // RES-007 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 4, - roommateRelations: 4, - facilitySatisfaction: 5, - safetyFeeling: 5, - positives: 'Ruhige Lage am See', - collectedBy: 'Frau Müller', - }, - }), - // ZH-004: Mixed — good for stable pair, flagged for departed RES-020 - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[8].id, // RES-014 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 4, - roommateRelations: 5, - facilitySatisfaction: 3, - safetyFeeling: 4, - positives: 'Mitbewohner ist super nett', - collectedBy: 'Herr Schmidt', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: endedPlacements[0].id, // RES-020 (ended) - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 2, - roommateRelations: 2, - facilitySatisfaction: 3, - safetyFeeling: 3, - concerns: 'Kann nachts nicht schlafen wegen Mitbewohner', - collectedBy: 'Herr Schmidt', - }, - }), - // ZH-006: Tension pair check-ins - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[12].id, // RES-016 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 3, - roommateRelations: 2, - facilitySatisfaction: 4, - safetyFeeling: 4, - concerns: 'Mitbewohnerin ist zu laut am Morgen', - collectedBy: 'Frau Müller', - }, - }), - prisma.satisfactionCheckIn.create({ - data: { - placementId: placements[13].id, // RES-017 - checkInType: 'INITIAL', - weekNumber: 1, - overallSatisfaction: 3, - roommateRelations: 3, - facilitySatisfaction: 4, - safetyFeeling: 4, - concerns: 'Mitbewohnerin geht sehr spät ins Bett, Licht stört', - collectedBy: 'Frau Müller', - }, - }), - ]) - - console.log(`✅ Created ${checkIns.length} satisfaction check-ins`) - - // ========================================================================= - // INCIDENTS (6 existing + 6 new = 12) - // ========================================================================= - - const incidents = await Promise.all([ - // --- Existing 6 incidents --- - prisma.incident.create({ - data: { - housingUnitId: units[0].id, - placementId: placements[0].id, - reportedById: residents[0].id, - subjectId: residents[1].id, - date: new Date(now.getTime() - 10 * DAY), - category: 'INTERPERSONAL', - type: 'NOISE_COMPLAINT', - severity: 'MEDIUM', - description: 'RES-001 beschwert sich über laute Musik nach 23 Uhr von RES-002', - resolution: 'Gespräch geführt, Kopfhörer-Regelung vereinbart', - resolvedAt: new Date(now.getTime() - 8 * DAY), - predictable: true, - compatibilityGap: 'lifestyle', - }, - }), - prisma.incident.create({ - data: { - housingUnitId: units[0].id, - placementId: placements[1].id, - subjectId: residents[1].id, - date: new Date(now.getTime() - 3 * DAY), - category: 'INTERPERSONAL', - type: 'SCHEDULE_CONFLICT', - severity: 'LOW', - description: 'Diskussion über Badezimmernutzung am Morgen', - predictable: true, - compatibilityGap: 'lifestyle', - }, - }), - prisma.incident.create({ - data: { - housingUnitId: units[1].id, - date: new Date(now.getTime() - 5 * DAY), - category: 'MAINTENANCE', - type: 'PLUMBING', - severity: 'MEDIUM', - description: 'Wasserhahn in der Küche tropft', - }, - }), - prisma.incident.create({ - data: { - housingUnitId: units[1].id, - date: new Date(now.getTime() - 2 * DAY), - category: 'MAINTENANCE', - type: 'HEATING_COOLING', - severity: 'HIGH', - description: 'Heizung im Zimmer von RES-006 funktioniert nicht richtig', - }, - }), - prisma.incident.create({ - data: { - housingUnitId: units[2].id, - date: new Date(now.getTime() - 14 * DAY), - category: 'MAINTENANCE', - type: 'ELECTRICAL', - severity: 'LOW', - description: 'Lampe im Flur defekt', - resolution: 'Leuchtmittel ersetzt', - resolvedAt: new Date(now.getTime() - 12 * DAY), - }, - }), - prisma.incident.create({ - data: { - housingUnitId: units[3].id, - date: new Date(now.getTime() - 60 * DAY), - category: 'MAINTENANCE', - type: 'APPLIANCE', - severity: 'MEDIUM', - description: 'Kühlschrank macht laute Geräusche', - resolution: 'Neuer Kühlschrank installiert', - resolvedAt: new Date(now.getTime() - 55 * DAY), - }, - }), - // --- 6 new incidents --- - // ZH-006: Interpersonal conflict between tension pair (correlates with low compatibility) - prisma.incident.create({ - data: { - housingUnitId: units[5].id, - placementId: placements[12].id, - reportedById: residents[15].id, // RES-016 - subjectId: residents[16].id, // RES-017 - date: new Date(now.getTime() - 18 * DAY), - category: 'INTERPERSONAL', - type: 'NOISE_COMPLAINT', - severity: 'MEDIUM', - description: 'RES-016 beschwert sich: RES-017 macht morgens um 5:30 Uhr Lärm in der Küche', - resolution: 'Vermittlungsgespräch, Küchenzeiten vereinbart', - resolvedAt: new Date(now.getTime() - 16 * DAY), - predictable: true, - compatibilityGap: 'lifestyle', - followUpPriority: 'NORMAL', - nextFollowUpDate: new Date(now.getTime() + 3 * DAY), - }, - }), - // ZH-006: Second incident — same tension pair, cleanliness - prisma.incident.create({ - data: { - housingUnitId: units[5].id, - placementId: placements[13].id, - reportedById: residents[15].id, // RES-016 - subjectId: residents[16].id, // RES-017 - date: new Date(now.getTime() - 7 * DAY), - category: 'INTERPERSONAL', - type: 'CLEANLINESS_DISPUTE', - severity: 'LOW', - description: 'Streit über Küchensauberkeit, RES-016 empfindet RES-017 als unordentlich', - predictable: true, - compatibilityGap: 'lifestyle', - }, - }), - // ZH-004: Safety concern - prisma.incident.create({ - data: { - housingUnitId: units[3].id, - date: new Date(now.getTime() - 12 * DAY), - category: 'SAFETY', - type: 'SAFETY_CONCERN', - severity: 'HIGH', - description: 'Eingangstür nachts nicht abgeschlossen vorgefunden', - resolution: 'Bewohner erinnert, automatischer Türschliesser wird installiert', - resolvedAt: new Date(now.getTime() - 10 * DAY), - followUpPriority: 'HIGH', - nextFollowUpDate: new Date(now.getTime() - 5 * DAY), - }, - }), - // ZH-004: Conflict involving RES-020 before transfer (resolved) - prisma.incident.create({ - data: { - housingUnitId: units[3].id, - placementId: endedPlacements[0].id, - reportedById: residents[17].id, // RES-018 - subjectId: residents[19].id, // RES-020 - date: new Date(now.getTime() - 50 * DAY), - category: 'INTERPERSONAL', - type: 'NOISE_COMPLAINT', - severity: 'HIGH', - description: 'RES-020 stört nachts Mitbewohner durch lautes Telefonieren', - resolution: 'Versetzung von RES-020 eingeleitet', - resolvedAt: new Date(now.getTime() - 40 * DAY), - predictable: true, - compatibilityGap: 'lifestyle', - }, - }), - // ZH-004: Second conflict with RES-020 - prisma.incident.create({ - data: { - housingUnitId: units[3].id, - placementId: endedPlacements[0].id, - subjectId: residents[19].id, // RES-020 - date: new Date(now.getTime() - 45 * DAY), - category: 'INTERPERSONAL', - type: 'PERSONAL_CONFLICT', - severity: 'MEDIUM', - description: 'Lautstärker Streit zwischen RES-020 und Mitbewohnern über Gemeinschaftsräume', - resolution: 'Vermittlungsgespräch, Versetzungsentscheid bestätigt', - resolvedAt: new Date(now.getTime() - 42 * DAY), - predictable: true, - compatibilityGap: 'social', - }, - }), - // ZH-001: Cultural friction (resolved positively — shows system tracking outcomes) - prisma.incident.create({ - data: { - housingUnitId: units[0].id, - placementId: placements[2].id, - reportedById: residents[21].id, // RES-022 - date: new Date(now.getTime() - 15 * DAY), - category: 'INTERPERSONAL', - type: 'CULTURAL_FRICTION', - severity: 'LOW', - description: 'Missverständnis über Küchennutzung wegen unterschiedlicher Gewohnheiten', - resolution: 'Küchenplan erstellt, Situation geklärt durch gemeinsames Kochen', - resolvedAt: new Date(now.getTime() - 13 * DAY), - predictable: false, - compatibilityGap: 'practical', - }, - }), - ]) - - console.log(`✅ Created ${incidents.length} incidents`) - - // Incident follow-ups - const followUps = await Promise.all([ - // Follow-up on ZH-006 noise complaint (incident index 6) - prisma.incidentFollowUp.create({ - data: { - incidentId: incidents[6].id, - action: 'Nachgespräch mit beiden Bewohnerinnen', - notes: 'Küchenzeiten werden eingehalten, Situation leicht verbessert', - outcome: 'Teilweise Verbesserung', - staffName: 'Frau Müller', - scheduledNextDate: new Date(now.getTime() + 7 * DAY), - }, - }), - // Follow-up on ZH-004 safety concern (incident index 8) - prisma.incidentFollowUp.create({ - data: { - incidentId: incidents[8].id, - action: 'Türschliesser installiert, alle Bewohner informiert', - notes: 'Automatischer Türschliesser funktioniert', - outcome: 'Problem behoben', - staffName: 'Herr Schmidt', - }, - }), - // Follow-up on RES-020 conflict (incident index 9) - prisma.incidentFollowUp.create({ - data: { - incidentId: incidents[9].id, - action: 'Versetzungsgespräch mit RES-020 durchgeführt', - notes: 'Bewohner versteht Gründe, akzeptiert Versetzung', - outcome: 'Versetzung durchgeführt', - staffName: 'Herr Schmidt', - }, - }), - ]) - - console.log(`✅ Created ${followUps.length} incident follow-ups`) - - // ========================================================================= - // MAINTENANCE REQUESTS - // ========================================================================= - - const maintenanceRequests = await Promise.all([ - // Open: plumbing issue in ZH-006 - prisma.maintenanceRequest.create({ - data: { - housingUnitId: units[5].id, - category: 'PLUMBING', - priority: 'NORMAL', - title: 'Dusche tropft', - description: 'Duschkopf in Badezimmer 1 tropft kontinuierlich', - location: 'Badezimmer 1', - reportedById: residents[15].id, - status: 'OPEN', - }, - }), - // Open: heating repair in ZH-002 - prisma.maintenanceRequest.create({ - data: { - housingUnitId: units[1].id, - category: 'HEATING_COOLING', - priority: 'HIGH', - title: 'Heizung Zimmer 2 defekt', - description: 'Heizung heizt nicht mehr richtig, Zimmer wird kalt', - location: 'Zimmer 2', - reportedById: residents[5].id, - status: 'ASSIGNED', - assignedTo: 'Hauswart Keller', - assignedAt: new Date(now.getTime() - 1 * DAY), - }, - }), - // Completed: electrical fix in ZH-003 - prisma.maintenanceRequest.create({ - data: { - housingUnitId: units[2].id, - category: 'ELECTRICAL', - priority: 'NORMAL', - title: 'Steckdose funktioniert nicht', - description: 'Steckdose neben Bett in Einzelzimmer 1 ohne Strom', - location: 'Einzelzimmer 1', - reportedById: residents[4].id, - status: 'COMPLETED', - assignedTo: 'Elektriker Meyer', - assignedAt: new Date(now.getTime() - 10 * DAY), - startedAt: new Date(now.getTime() - 8 * DAY), - completedAt: new Date(now.getTime() - 7 * DAY), - resolution: 'Sicherung war defekt, ersetzt', - cost: 85.0, - }, - }), - // Completed: appliance replacement in ZH-004 - prisma.maintenanceRequest.create({ - data: { - housingUnitId: units[3].id, - category: 'APPLIANCE', - priority: 'NORMAL', - title: 'Waschmaschine defekt', - description: 'Waschmaschine schleudert nicht mehr richtig', - location: 'Waschküche', - reporterName: 'Hauswart Keller', - status: 'COMPLETED', - assignedTo: 'Electrolux Service', - assignedAt: new Date(now.getTime() - 20 * DAY), - startedAt: new Date(now.getTime() - 15 * DAY), - completedAt: new Date(now.getTime() - 12 * DAY), - resolution: 'Neue Waschmaschine installiert', - cost: 890.0, - }, - }), - ]) - - console.log(`✅ Created ${maintenanceRequests.length} maintenance requests`) - - // ========================================================================= - // TRANSFER REQUESTS - // ========================================================================= - - const transferRequests = await Promise.all([ - // PENDING: RES-001 wants to move from ZH-001 to ZH-002 (awaiting staff review) - prisma.transferRequest.create({ - data: { - residentId: residents[0].id, - currentPlacementId: placements[0].id, - targetUnitId: units[1].id, - reason: - 'Meine aktuelle Wohngemeinschaft ist sehr laut und ich schlafe schlecht. Ich würde gerne in eine ruhigere Unterkunft wechseln.', - status: 'PENDING', - }, - }), - // APPROVED: RES-004 requested transfer from ZH-002 to ZH-003 (approved 5 days ago) - prisma.transferRequest.create({ - data: { - residentId: residents[3].id, - currentPlacementId: placements[4].id, - targetUnitId: units[2].id, - reason: - 'Ich möchte in eine kleinere Unterkunft wechseln, da ich mehr Privatsphäre benötige.', - status: 'APPROVED', - staffNotes: 'Transferanfrage genehmigt. Verlegung wird nächste Woche koordiniert.', - reviewedBy: `${BRAND.codePrefix}ADMIN1`, - reviewedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), - }, - }), - // DENIED: RES-006 requested transfer (denied — no suitable spot available) - prisma.transferRequest.create({ - data: { - residentId: residents[5].id, - currentPlacementId: placements[5].id, - reason: 'Ich möchte näher an meiner Sprachschule wohnen, um den Weg zu verkürzen.', - status: 'DENIED', - staffNotes: - 'Leider kein geeigneter Platz in der gewünschten Lage verfügbar. Bitte in 4 Wochen erneut anfragen.', - reviewedBy: `${BRAND.codePrefix}ADMIN1`, - reviewedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), - }, - }), - ]) - - console.log(`✅ Created ${transferRequests.length} transfer requests`) - - // ========================================================================= - // HOUSEHOLD TASKS - // ========================================================================= - - const householdTasks = await Promise.all([ - // Kitchen cleaning schedule (recurring) in ZH-001 - prisma.householdTask.create({ - data: { - housingUnitId: units[0].id, - title: 'Küche reinigen', - description: 'Arbeitsflächen, Herd und Spüle reinigen', - instructions: 'Alle Oberflächen abwischen, Herd reinigen, Spüle putzen, Boden wischen', - taskType: 'RECURRING_SCHEDULED', - category: 'CLEANING', - priority: 'NORMAL', - scheduleHuman: 'Jeden Montag und Donnerstag', - estimatedMinutes: 30, - currentStatus: 'IDLE', - createdByStaff: 'Frau Müller', - }, - }), - // Trash duty (recurring) in ZH-004 - prisma.householdTask.create({ - data: { - housingUnitId: units[3].id, - title: 'Müll rausbringen', - description: 'Kehrichtsack und Recycling zur Sammelstelle bringen', - instructions: 'Kehricht (grauer Sack) Dienstag, Papier/Karton Mittwoch, PET jederzeit', - taskType: 'RECURRING_SCHEDULED', - category: 'TRASH', - priority: 'NORMAL', - scheduleHuman: 'Jeden Dienstag (Kehricht), Mittwoch (Papier)', - estimatedMinutes: 15, - currentStatus: 'IDLE', - createdByStaff: 'Herr Schmidt', - }, - }), - // One-time deep clean in ZH-006 - prisma.householdTask.create({ - data: { - housingUnitId: units[5].id, - title: 'Grundreinigung Badezimmer', - description: 'Gründliche Reinigung beider Badezimmer', - instructions: 'Fliesen, Fugen, WC, Dusche, Waschbecken, Spiegel reinigen', - taskType: 'ONE_TIME', - category: 'CLEANING', - priority: 'HIGH', - estimatedMinutes: 60, - currentStatus: 'NEEDS_ATTENTION', - createdByStaff: 'Frau Müller', - }, - }), - // Kitchen cleaning (recurring) in ZH-002 - prisma.householdTask.create({ - data: { - housingUnitId: units[1].id, - title: 'Küche und Gemeinschaftsräume', - description: 'Wöchentliche Reinigung der Gemeinschaftsräume', - instructions: 'Küche reinigen, Wohnbereich staubsaugen, Oberflächen abwischen', - taskType: 'RECURRING_SCHEDULED', - category: 'CLEANING', - priority: 'NORMAL', - scheduleHuman: 'Jeden Freitag', - estimatedMinutes: 45, - currentStatus: 'IDLE', - createdByStaff: 'Herr Schmidt', - }, - }), - ]) - - console.log(`✅ Created ${householdTasks.length} household tasks`) - - // ========================================================================= - // UPDATE UNIT STATUSES - // ========================================================================= - - // ZH-001: 3/4 beds occupied - // ZH-002: 3/6 beds occupied - // ZH-003: 2/3 rooms occupied - // ZH-004: 4/8 beds occupied - // ZH-005: maintenance - // ZH-006: 3/6 beds occupied - // ZH-007: empty, available - // ZH-008: empty, available - - // Pilot baseline config (from CLAUDE.md example metrics — pre-system Phase-1 data) - const earliestPlacement = await prisma.placement.findFirst({ - orderBy: { startDate: 'asc' }, - select: { startDate: true }, - }) - await prisma.systemConfig.upsert({ - where: { id: 'singleton' }, - create: { - id: 'singleton', - pilotBaselineIncidentsPerMonth: 15, - pilotBaselineRelocationsPerMonth: 4, - pilotBaselineMediationHoursPerWeek: 12, - pilotStartDate: earliestPlacement?.startDate ?? new Date('2025-11-01'), - }, - update: { - pilotBaselineIncidentsPerMonth: 15, - pilotBaselineRelocationsPerMonth: 4, - pilotBaselineMediationHoursPerWeek: 12, - pilotStartDate: earliestPlacement?.startDate ?? new Date('2025-11-01'), - }, - }) - - // --------------------------------------------------------------------------- - // GOVERNANCE — AOZ rule catalog + a house that has started using it - // --------------------------------------------------------------------------- - - // The AOZ tier is reference data, not demo data: it must exist in production - // too. Idempotent, so re-seeding never duplicates or resets acknowledgements. - const ruleSync = await syncOrgRules(prisma) - - const quietRule = await prisma.houseRule.findUnique({ where: { key: 'night_quiet' } }) - const kitchenRule = await prisma.houseRule.findUnique({ where: { key: 'kitchen_use' } }) - const cleaningRule = await prisma.houseRule.findUnique({ where: { key: 'shared_cleaning' } }) - - const demoUnit = units[0] - let houseRuleCount = 0 - let proposalCount = 0 - - if (demoUnit && kitchenRule && quietRule && cleaningRule) { - // A house that has already decided one topic... - const existingHouseRule = await prisma.houseRule.findFirst({ - where: { scope: 'UNIT', housingUnitId: demoUnit.id, parentRuleId: kitchenRule.id }, - }) - - if (!existingHouseRule) { - await prisma.houseRule.create({ - data: { - scope: 'UNIT', - housingUnitId: demoUnit.id, - parentRuleId: kitchenRule.id, - category: kitchenRule.category, - title: 'Küche: abwaschen am selben Abend', - body: 'Wer kocht, wäscht am selben Abend ab. Geschirr, das über Nacht stehen bleibt, wird in eine Kiste neben der Spüle geräumt. Am Sonntag räumt die Person auf, die in der Woche dran war.', - delegation: kitchenRule.delegation, - status: 'ACTIVE', - version: 1, - }, - }) - houseRuleCount++ - } - - // ...and one still being decided, so the voting UI has something to show. - const demoUnitResidents = await prisma.placement.findMany({ - where: { housingUnitId: demoUnit.id, status: 'ACTIVE' }, - select: { residentId: true }, - }) - const voterIds = Array.from(new Set(demoUnitResidents.map((p) => p.residentId))) - - const existingProposal = await prisma.proposal.findFirst({ - where: { housingUnitId: demoUnit.id }, - }) - - if (!existingProposal && voterIds.length >= 3) { - const votingEndsAt = new Date() - votingEndsAt.setDate(votingEndsAt.getDate() + 4) - - const proposal = await prisma.proposal.create({ - data: { - housingUnitId: demoUnit.id, - type: 'ADD_RULE', - category: cleaningRule.category, - title: 'Putzplan mit fixem Wochentag', - body: 'Vorschlag: Jede Person übernimmt eine Woche lang Küche und Bad. Der Wechsel ist immer am Sonntagabend. Wer in seiner Woche verhindert ist, tauscht vorher mit jemandem.', - parentOrgRuleId: cleaningRule.id, - proposedByResidentId: voterIds[0], - status: 'VOTING', - decisionMode: 'RESIDENT_BINDING', - threshold: 'SIMPLE_MAJORITY', - quorumPercent: 50, - approvalPercent: 51, - eligibleVoterCount: voterIds.length, - votingOpenedAt: new Date(), - votingEndsAt, - }, - }) - proposalCount++ - - // Enough votes to be interesting but not yet decided. - await prisma.vote.createMany({ - data: [ - { proposalId: proposal.id, residentId: voterIds[0], choice: 'YES' }, - { proposalId: proposal.id, residentId: voterIds[1], choice: 'YES' }, - ], - skipDuplicates: true, - }) - } - } - - console.log('✅ Database seeded successfully!') - console.log('') - console.log('📊 Summary:') - console.log(` - ${units.length} housing units`) - const placed = residents.filter((r) => r.status === 'PLACED').length - const active = residents.filter((r) => r.status === 'ACTIVE').length - const transferred = residents.filter((r) => r.status === 'TRANSFERRED').length - console.log( - ` - ${residents.length} residents (${placed} placed, ${active} waiting, ${transferred} transferred)`, - ) - console.log(` - ${placements.length} active + ${endedPlacements.length} ended placements`) - console.log(` - ${assessments.length} compatibility assessments`) - console.log(` - ${checkIns.length} satisfaction check-ins`) - console.log(` - ${incidents.length} incidents (${followUps.length} follow-ups)`) - console.log(` - ${maintenanceRequests.length} maintenance requests`) - console.log(` - ${transferRequests.length} transfer requests (1 pending, 1 approved, 1 denied)`) - console.log(` - ${householdTasks.length} household tasks`) - console.log( - ` - ${ruleSync.created + ruleSync.unchanged + ruleSync.amended} AOZ rules ` + - `(${ruleSync.created} new), ${houseRuleCount} house rule(s), ${proposalCount} open decision(s)`, - ) - console.log('') - console.log('🚀 Ready to run: npm run dev') -} - -main() - .catch((e) => { - console.error('❌ Seed failed:', e) - process.exit(1) - }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/prisma/real/aoz-team.ts b/scripts/db/real/aoz-team.ts similarity index 96% rename from prisma/real/aoz-team.ts rename to scripts/db/real/aoz-team.ts index 62a4eb2f..77aa656f 100644 --- a/prisma/real/aoz-team.ts +++ b/scripts/db/real/aoz-team.ts @@ -25,7 +25,7 @@ * once. Committing them would publish three working staff logins. */ -import type { StaffRole, StaffScopeId } from '../../src/lib/auth/role-policy' +import type { StaffRole, StaffScopeId } from '../../../src/lib/auth/role-policy' export interface RealStaffSeed { /** Shown wherever staff are listed; never a bare code. */ diff --git a/prisma/real/witikonerstrasse-458.ts b/scripts/db/real/witikonerstrasse-458.ts similarity index 93% rename from prisma/real/witikonerstrasse-458.ts rename to scripts/db/real/witikonerstrasse-458.ts index c94fb02c..fccbd963 100644 --- a/prisma/real/witikonerstrasse-458.ts +++ b/scripts/db/real/witikonerstrasse-458.ts @@ -1,5 +1,5 @@ /** - * Real apartment: Witikonerstrasse 458 — data config for prisma/seed-real.ts. + * Real apartment: Witikonerstrasse 458 — data config for scripts/db/seed-real.ts. * * SSOT for the physical layout and who lives where. Login codes are NOT in * this file on purpose: they are generated at seed time and printed once — diff --git a/prisma/scoring-helper.ts b/scripts/db/scoring-helper.ts similarity index 94% rename from prisma/scoring-helper.ts rename to scripts/db/scoring-helper.ts index f6cea889..054fc49e 100644 --- a/prisma/scoring-helper.ts +++ b/scripts/db/scoring-helper.ts @@ -18,12 +18,12 @@ * Every demo, screenshot and "Algorithmus-Genauigkeit" panel built on that * data was therefore describing software that does not exist. The alias * problem is solved where it belongs — `ts-node -r tsconfig-paths/register` - * in the prisma seed command — and the algorithm has exactly one home again. + * in the db:seed command — and the algorithm has exactly one home again. * * Guarded by `src/lib/__tests__/scoring-ssot.test.ts`. */ -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import { calculateCompatibility } from '@/lib/compatibility/scoring' import { toResidentProfile } from '@/lib/compatibility/convert' diff --git a/prisma/seed-admin.ts b/scripts/db/seed-admin.ts similarity index 69% rename from prisma/seed-admin.ts rename to scripts/db/seed-admin.ts index ce215a99..9ec558d2 100644 --- a/prisma/seed-admin.ts +++ b/scripts/db/seed-admin.ts @@ -2,18 +2,16 @@ * Seed Initial Admin User (code-based auth) * * Usage: - * npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-admin.ts + * npm run db:seed:admin * * Or with custom code: - * ADMIN_CODE=AOCH-CUSTOM npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-admin.ts + * ADMIN_CODE=AOCH-CUSTOM npm run db:seed:admin */ -import { PrismaClient } from '@prisma/client' -// Relative, not '@/': this runs under ts-node, which does not apply tsconfig paths. -import { BRAND } from '../src/lib/config/brand' -import { WIDEST_CAPABILITIES } from '../src/lib/auth/role-policy' - -const prisma = new PrismaClient() +import { eq } from 'drizzle-orm' +import { db, user, account } from '@/lib/db' +import { BRAND } from '@/lib/config/brand' +import { WIDEST_CAPABILITIES } from '@/lib/auth/role-policy' // Derived from the active brand — a rebrand must not silently orphan the seeded // admin, which is exactly what happened when the default moved to AOCH. @@ -34,8 +32,8 @@ async function main() { console.log('Creating admin user...') // Check if admin already exists by code - const existingByCode = await prisma.user.findUnique({ - where: { code: ADMIN_CODE }, + const existingByCode = await db.query.user.findFirst({ + where: eq(user.code, ADMIN_CODE), }) if (existingByCode) { @@ -46,18 +44,15 @@ async function main() { // Email lives on the Account, not the User — an admin seeded under a former // code prefix is found through the account that carries the same email. const existingByEmail = ADMIN_EMAIL - ? await prisma.account.findUnique({ - where: { email: ADMIN_EMAIL.toLowerCase() }, - select: { userId: true }, + ? await db.query.account.findFirst({ + where: eq(account.email, ADMIN_EMAIL.toLowerCase()), + columns: { userId: true }, }) : null if (existingByEmail?.userId) { // Update existing user to have a code - await prisma.user.update({ - where: { id: existingByEmail.userId }, - data: { code: ADMIN_CODE }, - }) + await db.update(user).set({ code: ADMIN_CODE }).where(eq(user.id, existingByEmail.userId)) console.log(`Updated existing admin with code: ${ADMIN_CODE}`) return } @@ -73,14 +68,18 @@ async function main() { // and an unreachable settings page. The migration only backfilled rows that // already existed; seeding is the other way an admin is born, and it was not // updated. Spreading the SSOT means a third axis cannot repeat this. - const admin = await prisma.user.create({ - data: { - code: ADMIN_CODE, - name: ADMIN_NAME, - ...WIDEST_CAPABILITIES, - active: true, - account: { create: { email: ADMIN_EMAIL.toLowerCase() } }, - }, + const admin = await db.transaction(async (tx) => { + const [created] = await tx + .insert(user) + .values({ + code: ADMIN_CODE, + name: ADMIN_NAME, + ...WIDEST_CAPABILITIES, + active: true, + }) + .returning() + await tx.insert(account).values({ email: ADMIN_EMAIL.toLowerCase(), userId: created.id }) + return created }) console.log('Admin user created successfully!') @@ -94,10 +93,11 @@ async function main() { } main() + .then(() => { + // The pg Pool keeps the event loop alive — exit explicitly on success. + process.exit(0) + }) .catch((e) => { console.error('Error creating admin user:', e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/scripts/db/seed-demo.ts b/scripts/db/seed-demo.ts new file mode 100644 index 00000000..6c7e2291 --- /dev/null +++ b/scripts/db/seed-demo.ts @@ -0,0 +1,22 @@ +/** + * CLI wrapper for the demo reset — wipe + reseed the presentation dataset. + * + * The actual logic lives in src/lib/demo/reset.ts (SSOT), shared with the + * daily reset endpoint (api/cron/reset-demo). + * + * Run: npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS"}' scripts/db/seed-demo.ts + */ + +import { db } from '@/lib/db' +import { resetDemoData } from '@/lib/demo/reset' + +resetDemoData(db) + .then((summary) => { + console.log('✅ Demo data reset:', summary) + // The pg Pool keeps the event loop alive — exit explicitly on success. + process.exit(0) + }) + .catch((e) => { + console.error(e) + process.exit(1) + }) diff --git a/prisma/seed-real.ts b/scripts/db/seed-real.ts similarity index 65% rename from prisma/seed-real.ts rename to scripts/db/seed-real.ts index d2fd7fa3..8b26d5ec 100644 --- a/prisma/seed-real.ts +++ b/scripts/db/seed-real.ts @@ -1,8 +1,8 @@ /** - * Seed a REAL apartment (no demo data) from prisma/real/*.ts config. + * Seed a REAL apartment (no demo data) from scripts/db/real/*.ts config. * * Usage: - * npx ts-node --compiler-options '{"module":"CommonJS"}' prisma/seed-real.ts [--wipe] + * npx ts-node -r tsconfig-paths/register --compiler-options '{"module":"CommonJS"}' scripts/db/seed-real.ts [--wipe] * * --wipe first truncates every table except User/AlgorithmWeight/SystemConfig * (same keep-list as the demo reset) — use it exactly once, when converting a @@ -15,18 +15,22 @@ * them; they are intentionally not committed anywhere. */ -import { PrismaClient } from '@prisma/client' +import { eq } from 'drizzle-orm' +import { db, housingUnit, placement, placementSpot, resident as residentTable } from '@/lib/db' import { REAL_APARTMENT } from './real/witikonerstrasse-458' -import { generateResidentCode } from '../src/lib/auth/code-generation' -import { wipeAllExceptKeepList } from '../src/lib/demo/wipe' -import { syncOrgRules } from '../src/lib/governance/sync-org-rules' - -const prisma = new PrismaClient() +import { generateResidentCode } from '@/lib/auth/code-generation' +import { wipeAllExceptKeepList } from '@/lib/demo/wipe' +import { syncOrgRules } from '@/lib/governance/sync-org-rules' async function uniqueResidentCode(): Promise { for (let attempt = 0; attempt < 10; attempt++) { const code = generateResidentCode() - if (!(await prisma.resident.findUnique({ where: { code }, select: { id: true } }))) { + if ( + !(await db.query.resident.findFirst({ + where: eq(residentTable.code, code), + columns: { id: true }, + })) + ) { return code } } @@ -36,13 +40,14 @@ async function uniqueResidentCode(): Promise { async function main() { const wipe = process.argv.includes('--wipe') if (wipe) { - const wiped = await wipeAllExceptKeepList(prisma) + const wiped = await wipeAllExceptKeepList(db) console.log(`🧹 Wiped ${wiped} tables (keep-list preserved)`) } - const existing = await prisma.housingUnit.findUnique({ - where: { code: REAL_APARTMENT.unit.code }, - select: { id: true, placements: { where: { status: 'ACTIVE' }, select: { id: true } } }, + const existing = await db.query.housingUnit.findFirst({ + where: eq(housingUnit.code, REAL_APARTMENT.unit.code), + columns: { id: true }, + with: { placements: { where: eq(placement.status, 'ACTIVE'), columns: { id: true } } }, }) if (existing && existing.placements.length > 0) { console.error( @@ -51,35 +56,38 @@ async function main() { process.exit(1) } - const unit = await prisma.housingUnit.create({ - data: { ...REAL_APARTMENT.unit, status: 'FULL' }, - }) + const [unit] = await db + .insert(housingUnit) + .values({ ...REAL_APARTMENT.unit, status: 'FULL' }) + .returning() // Rooms with their beds (the spot hierarchy the rest of the app expects). const bedsByRoom = new Map() for (const room of REAL_APARTMENT.rooms) { - const roomSpot = await prisma.placementSpot.create({ - data: { + const [roomSpot] = await db + .insert(placementSpot) + .values({ housingUnitId: unit.id, code: room.code, label: room.label, type: 'ROOM', capacity: room.beds, status: 'OCCUPIED', - }, - }) + }) + .returning() const bedIds: string[] = [] for (let i = 1; i <= room.beds; i++) { - const bed = await prisma.placementSpot.create({ - data: { + const [bed] = await db + .insert(placementSpot) + .values({ housingUnitId: unit.id, code: `${room.code}-B${i}`, type: 'BED', parentSpotId: roomSpot.id, capacity: 1, status: 'AVAILABLE', - }, - }) + }) + .returning() bedIds.push(bed.id) } bedsByRoom.set(room.code, bedIds) @@ -96,8 +104,9 @@ async function main() { const bedId = bedIds.shift()! const code = await uniqueResidentCode() - const resident = await prisma.resident.create({ - data: { + const [resident] = await db + .insert(residentTable) + .values({ code, displayName: person.displayName, // Neutral defaults — everyone refines their own preferences in the @@ -114,24 +123,22 @@ async function main() { mobilityNeeds: 'NONE', privacyNeed: 3, status: 'PLACED', - }, - }) + }) + .returning() - await prisma.placement.create({ - data: { - residentId: resident.id, - housingUnitId: unit.id, - spotId: bedId, - startDate, - status: 'ACTIVE', - }, + await db.insert(placement).values({ + residentId: resident.id, + housingUnitId: unit.id, + spotId: bedId, + startDate, + status: 'ACTIVE', }) - await prisma.placementSpot.update({ where: { id: bedId }, data: { status: 'OCCUPIED' } }) + await db.update(placementSpot).set({ status: 'OCCUPIED' }).where(eq(placementSpot.id, bedId)) credentials.push({ name: person.displayName, code, room: person.room }) } - await syncOrgRules(prisma) + await syncOrgRules(db) console.log(`\n🏠 Seeded ${REAL_APARTMENT.unit.code} (${REAL_APARTMENT.unit.address})`) console.log('\nLogin codes — hand these out, they are shown ONCE:\n') @@ -142,10 +149,11 @@ async function main() { } main() + .then(() => { + // The pg Pool keeps the event loop alive — exit explicitly on success. + process.exit(0) + }) .catch((e) => { console.error(e) process.exit(1) }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/scripts/db/seed.ts b/scripts/db/seed.ts new file mode 100644 index 00000000..d4e648f5 --- /dev/null +++ b/scripts/db/seed.ts @@ -0,0 +1,2754 @@ +/** + * Seed script for AOZ Housing + * + * Run with: npm run db:seed + */ + +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' +import { and, asc, eq } from 'drizzle-orm' +import type { PgTable } from 'drizzle-orm/pg-core' +import { + db, + algorithmWeight, + compatibilityAssessment, + householdTask, + houseRule, + housingUnit, + incident, + incidentFollowUp, + incidentInvolvement, + maintenanceRequest, + opportunity, + opportunityApplication, + placement, + placementSpot, + proposal as proposalTable, + resident, + satisfactionCheckIn, + systemConfig, + taskAttentionFlag, + taskCompletion, + taskRequest, + transferRequest, + user, + vote, +} from '@/lib/db' +import { calculateScore } from './scoring-helper' +import { syncOrgRules } from '@/lib/governance/sync-org-rules' +import { seedIntegrationEvidence } from '@/lib/seed/integration-evidence' +import { seedOpportunities } from '@/lib/seed/opportunities' +import { BRAND } from '@/lib/config/brand' + +/** Insert one row and return it — the seed's replacement for `prisma.x.create`. */ +async function createRow( + table: T, + data: InferInsertModel, +): Promise> { + const rows = await db + .insert(table) + .values(data as never) + .returning() + return rows[0] as InferSelectModel +} + +async function main() { + console.log('🌱 Seeding database...') + + // Clean existing data (order matters: FK constraints) + // Applications hold a Restrict on Opportunity, so they go first — and both + // go before residents, whose delete would otherwise be vetoed. + await db.delete(opportunityApplication) + await db.delete(opportunity) + await db.delete(taskRequest) + await db.delete(taskAttentionFlag) + await db.delete(taskCompletion) + await db.delete(householdTask) + await db.delete(maintenanceRequest) + await db.delete(incidentFollowUp) + await db.delete(satisfactionCheckIn) + await db.delete(incidentInvolvement) + await db.delete(incident) + await db.delete(transferRequest) + await db.delete(compatibilityAssessment) + await db.delete(placement) + await db.delete(placementSpot) + await db.delete(resident) + await db.delete(housingUnit) + await db.delete(algorithmWeight) + + // Create algorithm weights + await createRow(algorithmWeight, { + lifestyleWeight: 30, + socialWeight: 25, + practicalWeight: 25, + riskWeight: 20, + factorWeights: { + sleep: 40, + noise: 30, + cleanliness: 30, + socialStyle: 35, + language: 40, + privacy: 25, + smoking: 40, + sharedSpaces: 30, + pets: 15, + dietary: 15, + }, + active: true, + notes: 'Initial weights', + }) + + // Create housing units + const units = await Promise.all([ + createRow(housingUnit, { + code: 'ZH-001', + address: 'Langstrasse 42, 8004 Zürich', + totalBeds: 4, + totalRooms: 2, + sharedRooms: 2, + privateRooms: 0, + sharedBathrooms: 1, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: false, + wheelchairAccess: false, + elevator: true, + smokingAllowed: false, + petsAllowed: false, + quietHours: '22:00-07:00', + nearPublicTransport: true, + nearHealthServices: true, + nearSchools: false, + status: 'AVAILABLE', + notes: 'Zentrale Lage, gute ÖV-Anbindung', + }), + createRow(housingUnit, { + code: 'ZH-002', + address: 'Badenerstrasse 120, 8004 Zürich', + totalBeds: 6, + totalRooms: 3, + sharedRooms: 2, + privateRooms: 1, + sharedBathrooms: 2, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: true, + wheelchairAccess: true, + elevator: false, + smokingAllowed: false, + petsAllowed: true, + quietHours: '22:00-07:00', + nearPublicTransport: true, + nearHealthServices: false, + nearSchools: true, + status: 'AVAILABLE', + notes: 'Erdgeschoss, barrierefrei', + }), + createRow(housingUnit, { + code: 'ZH-003', + address: 'Seestrasse 55, 8002 Zürich', + totalBeds: 3, + totalRooms: 3, + sharedRooms: 0, + privateRooms: 3, + sharedBathrooms: 1, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: false, + wheelchairAccess: false, + elevator: true, + smokingAllowed: false, + petsAllowed: false, + quietHours: '21:00-08:00', + nearPublicTransport: true, + nearHealthServices: true, + nearSchools: false, + status: 'AVAILABLE', + notes: 'Ruhige Lage am See, alle Einzelzimmer', + }), + createRow(housingUnit, { + code: 'ZH-004', + address: 'Hardstrasse 88, 8005 Zürich', + totalBeds: 8, + totalRooms: 4, + sharedRooms: 4, + privateRooms: 0, + sharedBathrooms: 2, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: false, + wheelchairAccess: false, + elevator: false, + smokingAllowed: true, + petsAllowed: false, + quietHours: '23:00-06:00', + nearPublicTransport: true, + nearHealthServices: false, + nearSchools: false, + status: 'AVAILABLE', + notes: 'Grössere Unterkunft, Rauchen auf Balkon erlaubt', + }), + createRow(housingUnit, { + code: 'ZH-005', + address: 'Militärstrasse 30, 8004 Zürich', + totalBeds: 2, + totalRooms: 1, + sharedRooms: 1, + privateRooms: 0, + sharedBathrooms: 1, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: true, + wheelchairAccess: false, + elevator: false, + smokingAllowed: false, + petsAllowed: false, + quietHours: '22:00-07:00', + nearPublicTransport: true, + nearHealthServices: false, + nearSchools: true, + status: 'MAINTENANCE', + notes: 'Kleine Einheit, derzeit Renovation', + }), + createRow(housingUnit, { + code: 'ZH-006', + address: 'Birmensdorferstrasse 65, 8004 Zürich', + totalBeds: 6, + totalRooms: 3, + sharedRooms: 3, + privateRooms: 0, + sharedBathrooms: 2, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: false, + wheelchairAccess: false, + elevator: true, + smokingAllowed: false, + petsAllowed: false, + quietHours: '22:00-07:00', + nearPublicTransport: true, + nearHealthServices: false, + nearSchools: true, + status: 'AVAILABLE', + notes: 'Grosse gemischte Unterkunft, gute Lage', + }), + createRow(housingUnit, { + code: 'ZH-007', + address: 'Hohlstrasse 192, 8004 Zürich', + totalBeds: 4, + totalRooms: 3, + sharedRooms: 2, + privateRooms: 0, + sharedBathrooms: 1, + privateBathrooms: 1, + sharedKitchen: true, + privateKitchen: true, + groundFloor: false, + wheelchairAccess: false, + elevator: true, + smokingAllowed: false, + petsAllowed: false, + quietHours: '22:00-07:00', + nearPublicTransport: true, + nearHealthServices: true, + nearSchools: false, + status: 'AVAILABLE', + notes: 'Studio-Option für besondere Bedürfnisse', + }), + createRow(housingUnit, { + code: 'ZH-008', + address: 'Josefstrasse 28, 8005 Zürich', + totalBeds: 3, + totalRooms: 3, + sharedRooms: 0, + privateRooms: 3, + sharedBathrooms: 1, + privateBathrooms: 0, + sharedKitchen: true, + privateKitchen: false, + groundFloor: true, + wheelchairAccess: true, + elevator: false, + smokingAllowed: false, + petsAllowed: false, + quietHours: '21:00-08:00', + nearPublicTransport: true, + nearHealthServices: true, + nearSchools: false, + status: 'AVAILABLE', + notes: 'Barrierefrei, Erdgeschoss, alle Einzelzimmer, medizintauglich', + }), + ]) + + console.log(`✅ Created ${units.length} housing units`) + + // Create placement spots for each unit + // ZH-001: 2 rooms, 4 beds total (2 beds per room) + const zh001Room1 = await createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 12, + floor: 2, + capacity: 2, + status: 'AVAILABLE', + }) + const zh001Room2 = await createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R2', + label: 'Zimmer 2', + type: 'ROOM', + squareMeters: 10, + floor: 2, + capacity: 2, + status: 'AVAILABLE', + }) + const zh001Beds = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R1-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh001Room1.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R1-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh001Room1.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R2-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh001Room2.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[0].id, + code: 'R2-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh001Room2.id, + status: 'AVAILABLE', + }), + ]) + + // ZH-002: 3 rooms - 2 shared (2 beds each) + 1 private room (medical) + const zh002Room1 = await createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 14, + floor: 0, + capacity: 2, + status: 'AVAILABLE', + }) + const zh002Room2 = await createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R2', + label: 'Zimmer 2', + type: 'ROOM', + squareMeters: 12, + floor: 0, + capacity: 2, + status: 'AVAILABLE', + }) + const zh002PrivateRoom = await createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R3', + label: 'Einzelzimmer', + type: 'PRIVATE_ROOM', + squareMeters: 10, + floor: 0, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }) + const zh002Beds = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R1-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh002Room1.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R1-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh002Room1.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R2-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh002Room2.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[1].id, + code: 'R2-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh002Room2.id, + status: 'AVAILABLE', + }), + ]) + + // ZH-003: 3 private rooms (all medical) + const zh003Rooms = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[2].id, + code: 'R1', + label: 'Einzelzimmer 1', + type: 'PRIVATE_ROOM', + squareMeters: 12, + floor: 3, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[2].id, + code: 'R2', + label: 'Einzelzimmer 2', + type: 'PRIVATE_ROOM', + squareMeters: 10, + floor: 3, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[2].id, + code: 'R3', + label: 'Einzelzimmer 3', + type: 'PRIVATE_ROOM', + squareMeters: 11, + floor: 3, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + ]) + + // ZH-004: 4 rooms, 8 beds total (2 beds per room) + const zh004Rooms = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[3].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 10, + floor: 1, + capacity: 2, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[3].id, + code: 'R2', + label: 'Zimmer 2', + type: 'ROOM', + squareMeters: 10, + floor: 1, + capacity: 2, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[3].id, + code: 'R3', + label: 'Zimmer 3', + type: 'ROOM', + squareMeters: 12, + floor: 2, + capacity: 2, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[3].id, + code: 'R4', + label: 'Zimmer 4', + type: 'ROOM', + squareMeters: 12, + floor: 2, + capacity: 2, + status: 'AVAILABLE', + }), + ]) + const zh004Beds = await Promise.all( + zh004Rooms.flatMap((room, idx) => [ + createRow(placementSpot, { + housingUnitId: units[3].id, + code: `R${idx + 1}-B1`, + label: 'Bett A', + type: 'BED', + parentSpotId: room.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[3].id, + code: `R${idx + 1}-B2`, + label: 'Bett B', + type: 'BED', + parentSpotId: room.id, + status: 'AVAILABLE', + }), + ]), + ) + + // ZH-005: 1 room, 2 beds (in maintenance) + const zh005Room = await createRow(placementSpot, { + housingUnitId: units[4].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 10, + floor: 0, + capacity: 2, + status: 'MAINTENANCE', + }) + const zh005Beds = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[4].id, + code: 'R1-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh005Room.id, + status: 'MAINTENANCE', + }), + createRow(placementSpot, { + housingUnitId: units[4].id, + code: 'R1-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh005Room.id, + status: 'MAINTENANCE', + }), + ]) + + // ZH-006: 3 rooms, 6 beds (2 beds per room) + const zh006Rooms = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[5].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 12, + floor: 1, + capacity: 2, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[5].id, + code: 'R2', + label: 'Zimmer 2', + type: 'ROOM', + squareMeters: 11, + floor: 1, + capacity: 2, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[5].id, + code: 'R3', + label: 'Zimmer 3', + type: 'ROOM', + squareMeters: 10, + floor: 2, + capacity: 2, + status: 'AVAILABLE', + }), + ]) + const zh006Beds = await Promise.all( + zh006Rooms.flatMap((room, idx) => [ + createRow(placementSpot, { + housingUnitId: units[5].id, + code: `R${idx + 1}-B1`, + label: 'Bett A', + type: 'BED', + parentSpotId: room.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[5].id, + code: `R${idx + 1}-B2`, + label: 'Bett B', + type: 'BED', + parentSpotId: room.id, + status: 'AVAILABLE', + }), + ]), + ) + + // ZH-007: 2 rooms (2 beds, 1 bed) + 1 studio + const zh007Room1 = await createRow(placementSpot, { + housingUnitId: units[6].id, + code: 'R1', + label: 'Zimmer 1', + type: 'ROOM', + squareMeters: 14, + floor: 1, + capacity: 2, + status: 'AVAILABLE', + }) + const zh007Room2 = await createRow(placementSpot, { + housingUnitId: units[6].id, + code: 'R2', + label: 'Zimmer 2', + type: 'ROOM', + squareMeters: 10, + floor: 1, + capacity: 1, + status: 'AVAILABLE', + }) + const zh007Studio = await createRow(placementSpot, { + housingUnitId: units[6].id, + code: 'STUDIO-A', + label: 'Studio', + type: 'STUDIO', + squareMeters: 18, + floor: 2, + capacity: 1, + hasPrivateBathroom: true, + hasPrivateKitchen: true, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }) + const zh007Beds = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[6].id, + code: 'R1-B1', + label: 'Bett A', + type: 'BED', + parentSpotId: zh007Room1.id, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[6].id, + code: 'R1-B2', + label: 'Bett B', + type: 'BED', + parentSpotId: zh007Room1.id, + status: 'AVAILABLE', + }), + ]) + + // ZH-008: 3 private rooms, ground floor (accessible, medical-eligible) + const zh008Rooms = await Promise.all([ + createRow(placementSpot, { + housingUnitId: units[7].id, + code: 'R1', + label: 'Einzelzimmer 1', + type: 'PRIVATE_ROOM', + squareMeters: 14, + floor: 0, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[7].id, + code: 'R2', + label: 'Einzelzimmer 2', + type: 'PRIVATE_ROOM', + squareMeters: 12, + floor: 0, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + createRow(placementSpot, { + housingUnitId: units[7].id, + code: 'R3', + label: 'Einzelzimmer 3', + type: 'PRIVATE_ROOM', + squareMeters: 12, + floor: 0, + capacity: 1, + requiresMedicalDocs: true, + status: 'AVAILABLE', + }), + ]) + + // Collect all spots for later use + const allSpots = { + zh001: { rooms: [zh001Room1, zh001Room2], beds: zh001Beds }, + zh002: { rooms: [zh002Room1, zh002Room2], privateRoom: zh002PrivateRoom, beds: zh002Beds }, + zh003: { rooms: zh003Rooms }, + zh004: { rooms: zh004Rooms, beds: zh004Beds }, + zh005: { room: zh005Room, beds: zh005Beds }, + zh006: { rooms: zh006Rooms, beds: zh006Beds }, + zh007: { rooms: [zh007Room1, zh007Room2], studio: zh007Studio, beds: zh007Beds }, + zh008: { rooms: zh008Rooms }, + } + + console.log(`✅ Created placement spots for ${units.length} units`) + + // Create residents + const residents = await Promise.all([ + // Placed residents + createRow(resident, { + code: 'RES-001', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['ar', 'en'], + culturalRegion: 'Middle East', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 4, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Studiert Informatik an der ETH', + }), + createRow(resident, { + code: 'RES-002', + ageRange: 'ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'NIGHT_OWL', + noiseTolerance: 4, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'EXTROVERTED', + languages: ['ar', 'fr'], + culturalRegion: 'Middle East', + smokingStatus: 'OUTDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 3, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Arbeitet als Koch, Spätschicht', + }), + createRow(resident, { + code: 'RES-003', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 2, + cleanlinessPractice: 5, + cleanlinessExpectation: 5, + chaosTolerance: 6 - 5, + socialStyle: 'INTROVERTED', + languages: ['uk', 'ru', 'en'], + culturalRegion: 'Eastern Europe', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: false, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 5, + choresContribution: 5, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'NEEDS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Ärztin, wartet auf Anerkennung', + // Medical docs for private room eligibility + hasMedicalDocumentation: true, + medicalDocType: 'PRIVATE_ROOM', + medicalDocDate: new Date(Date.now() - 100 * 24 * 60 * 60 * 1000), + medicalDocNotes: 'Benötigt Einzelzimmer aufgrund erhöhtem Privatsphärebedürfnis', + }), + createRow(resident, { + code: 'RES-004', + ageRange: 'MIDDLE_AGED', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['ti', 'en'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 4, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Gelernter Elektriker', + }), + createRow(resident, { + code: 'RES-005', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 4, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'EXTROVERTED', + languages: ['fa', 'en'], + culturalRegion: 'Central Asia', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 3, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: true, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'ELEVATED', + status: 'PLACED', + notes: 'Macht Deutschkurs B1', + // Medical docs for private room eligibility (in ZH-003) + hasMedicalDocumentation: true, + medicalDocType: 'BOTH', + medicalDocDate: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000), + medicalDocNotes: 'Psychologische Empfehlung für ruhige Umgebung', + }), + createRow(resident, { + code: 'RES-006', + ageRange: 'SENIOR', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 1, + cleanlinessPractice: 5, + cleanlinessExpectation: 5, + chaosTolerance: 6 - 5, + socialStyle: 'INTROVERTED', + languages: ['tr', 'de'], + culturalRegion: 'Middle East', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'GROUND_FLOOR', + medicalEquipment: true, + petTolerance: false, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 5, + choresContribution: 2, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: true, + supportLevel: 'ELEVATED', + status: 'PLACED', + notes: 'Pensioniert, braucht CPAP-Gerät nachts', + }), + createRow(resident, { + code: 'RES-007', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['so', 'ar', 'en'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 4, + choresContribution: 5, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'NEEDS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Arbeitet Teilzeit im Reinigungsbereich', + // Medical docs for private room eligibility (in ZH-003) + hasMedicalDocumentation: true, + medicalDocType: 'PRIVATE_ROOM', + medicalDocDate: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000), + medicalDocNotes: 'Ärztliches Attest für Einzelzimmer', + }), + // Unplaced residents (waiting for placement) + createRow(resident, { + code: 'RES-008', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'NIGHT_OWL', + noiseTolerance: 5, + cleanlinessPractice: 2, + cleanlinessExpectation: 2, + chaosTolerance: 6 - 2, + socialStyle: 'EXTROVERTED', + languages: ['ps', 'fa'], + culturalRegion: 'Central Asia', + smokingStatus: 'INDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 1, + choresContribution: 2, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Neu angekommen, braucht Raucherunterkunft', + }), + createRow(resident, { + code: 'RES-009', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 2, + cleanlinessPractice: 5, + cleanlinessExpectation: 5, + chaosTolerance: 6 - 5, + socialStyle: 'INTROVERTED', + languages: ['uk', 'en'], + culturalRegion: 'Eastern Europe', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['vegetarian'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 5, + choresContribution: 5, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'IT-Fachfrau, sucht ruhige Unterkunft', + }), + createRow(resident, { + code: 'RES-010', + ageRange: 'MIDDLE_AGED', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'IRREGULAR', + noiseTolerance: 3, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'MODERATE', + languages: ['ar', 'en', 'de'], + culturalRegion: 'Middle East', + smokingStatus: 'OUTDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 3, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Taxifahrer, unregelmässige Arbeitszeiten', + }), + // --- New residents (RES-011 to RES-025) --- + // RES-011: Hard-to-place — night owl, indoor smoker + createRow(resident, { + code: 'RES-011', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'NIGHT_OWL', + noiseTolerance: 4, + cleanlinessPractice: 2, + cleanlinessExpectation: 2, + chaosTolerance: 6 - 2, + socialStyle: 'MODERATE', + languages: ['ps', 'fa'], + culturalRegion: 'Central Asia', + smokingStatus: 'INDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 2, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Raucht viel, schwer zu platzieren', + }), + // RES-012: Easy-to-place — clean, quiet, cooperative + createRow(resident, { + code: 'RES-012', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 2, + cleanlinessPractice: 5, + cleanlinessExpectation: 5, + chaosTolerance: 6 - 5, + socialStyle: 'MODERATE', + languages: ['ti', 'en'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 5, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Ordentlich, kooperativ, leicht zu platzieren', + }), + // RES-013: Accessibility needs — senior, ground floor, medical equipment + createRow(resident, { + code: 'RES-013', + ageRange: 'SENIOR', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 2, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'INTROVERTED', + languages: ['ar'], + culturalRegion: 'Middle East', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'GROUND_FLOOR', + medicalEquipment: true, + petTolerance: false, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 4, + choresContribution: 2, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: true, + supportLevel: 'ELEVATED', + status: 'ACTIVE', + notes: 'Benötigt Erdgeschoss und Platz für medizinische Geräte', + hasMedicalDocumentation: true, + medicalDocType: 'BOTH', + medicalDocDate: new Date(Date.now() - 20 * 24 * 60 * 60 * 1000), + medicalDocNotes: 'Ärztliche Empfehlung für Erdgeschoss und Einzelzimmer', + }), + // RES-014: Good match in ZH-004 — extroverted, standard + createRow(resident, { + code: 'RES-014', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 4, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'EXTROVERTED', + languages: ['es', 'en'], + culturalRegion: 'South America', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 3, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Fussballfan, sucht Anschluss', + }), + // RES-015: Good match pair with RES-014 in ZH-004 + createRow(resident, { + code: 'RES-015', + ageRange: 'ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'MODERATE', + languages: ['fr', 'sw', 'en'], + culturalRegion: 'Central Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 4, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Gelernter Mechaniker, spricht drei Sprachen', + }), + // RES-016: Tension pair with RES-017 — night owl, introverted + createRow(resident, { + code: 'RES-016', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'NIGHT_OWL', + noiseTolerance: 2, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'INTROVERTED', + languages: ['fa', 'en'], + culturalRegion: 'Middle East', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: false, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 5, + choresContribution: 4, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Studiert abends, braucht Ruhe', + }), + // RES-017: Tension pair with RES-016 — early bird, extroverted + createRow(resident, { + code: 'RES-017', + ageRange: 'ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 4, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'EXTROVERTED', + languages: ['am', 'en'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 3, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Gesellig, kocht gerne für andere', + }), + // RES-018: Stable in ZH-004 + createRow(resident, { + code: 'RES-018', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'MODERATE', + languages: ['so', 'ar'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 3, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Besucht Integrationskurs', + }), + // RES-019: Stable in ZH-004 — speaks German, helps others + createRow(resident, { + code: 'RES-019', + ageRange: 'MIDDLE_AGED', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['tr', 'de'], + culturalRegion: 'Middle East', + smokingStatus: 'OUTDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 4, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Spricht gut Deutsch, hilft anderen Bewohnern', + }), + // RES-020: Failed placement → transfer story + createRow(resident, { + code: 'RES-020', + ageRange: 'ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'NIGHT_OWL', + noiseTolerance: 4, + cleanlinessPractice: 2, + cleanlinessExpectation: 2, + chaosTolerance: 6 - 2, + socialStyle: 'EXTROVERTED', + languages: ['ar', 'en'], + culturalRegion: 'Middle East', + smokingStatus: 'OUTDOOR_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 1, + choresContribution: 2, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: true, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'ELEVATED', + status: 'TRANSFERRED', + notes: 'Versetzt nach Konflikten in ZH-004', + }), + // RES-021: Recently arrived, waiting + createRow(resident, { + code: 'RES-021', + ageRange: 'YOUNG_ADULT', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['es', 'en'], + culturalRegion: 'South America', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 4, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Vor kurzem angekommen, wartet auf Platzierung', + }), + // RES-022: Success story in ZH-001 + createRow(resident, { + code: 'RES-022', + ageRange: 'ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 3, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['ti', 'en', 'ar'], + culturalRegion: 'East Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 3, + choresContribution: 5, + recyclingKnowledge: 'GOOD', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + conflictStyle: 'COOPERATIVE', + status: 'PLACED', + notes: 'Zuverlässig, hilft bei Haushaltsaufgaben', + }), + // RES-023: Complex needs, waiting + createRow(resident, { + code: 'RES-023', + ageRange: 'MIDDLE_AGED', + gender: 'FEMALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 2, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'INTROVERTED', + languages: ['fa', 'ps'], + culturalRegion: 'Central Asia', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: false, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 4, + choresContribution: 3, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: true, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'ELEVATED', + status: 'ACTIVE', + notes: 'Erhöhter Betreuungsbedarf, braucht ruhige Umgebung', + }), + // RES-024: Transferred into better match (was ZH-001 → now ZH-006) + createRow(resident, { + code: 'RES-024', + ageRange: 'YOUNG_ADULT', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'STANDARD', + noiseTolerance: 4, + cleanlinessPractice: 3, + cleanlinessExpectation: 3, + chaosTolerance: 6 - 3, + socialStyle: 'EXTROVERTED', + languages: ['ar', 'en', 'de'], + culturalRegion: 'Middle East', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: ['halal'], + mobilityNeeds: 'NONE', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 2, + choresContribution: 3, + recyclingKnowledge: 'BASIC', + roomSharingStatus: 'CAN_SHARE', + hasNightDisturbances: false, + needsQuietEnvironment: false, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'PLACED', + notes: 'Spricht drei Sprachen, Deutschkurs A2', + }), + // RES-025: Accessibility needs, waiting + createRow(resident, { + code: 'RES-025', + ageRange: 'SENIOR', + gender: 'MALE', + familyStatus: 'SINGLE', + sleepSchedule: 'EARLY_BIRD', + noiseTolerance: 2, + cleanlinessPractice: 4, + cleanlinessExpectation: 4, + chaosTolerance: 6 - 4, + socialStyle: 'MODERATE', + languages: ['fr', 'sw'], + culturalRegion: 'Central Africa', + smokingStatus: 'NON_SMOKER', + dietaryNeeds: [], + mobilityNeeds: 'GROUND_FLOOR', + medicalEquipment: false, + petTolerance: true, + sharedBathroom: true, + sharedKitchen: true, + privacyNeed: 4, + choresContribution: 3, + recyclingKnowledge: 'NONE', + roomSharingStatus: 'PREFERS_PRIVATE', + hasNightDisturbances: false, + needsQuietEnvironment: true, + hasSleepEquipment: false, + supportLevel: 'STANDARD', + status: 'ACTIVE', + notes: 'Benötigt Erdgeschoss, wartet auf passende Unterkunft', + }), + ]) + + console.log(`✅ Created ${residents.length} residents`) + + // Integration evidence — language, qualification and volunteering records, + // derived from each resident's own profile. Without it /learning renders + // five zeroes and an empty list on a database holding 24 people, and the + // dashboard's learning pulse reports nothing to every coaching role. + // + // Care seats go to whichever staff account this database already has (the + // admin from scripts/db/seed-admin.ts); with none, evidence still seeds and the + // seats stay empty rather than conjuring a colleague. + const seedStaff = await db.query.user.findFirst({ + where: eq(user.active, true), + columns: { id: true }, + orderBy: [asc(user.code)], + }) + const integration = await seedIntegrationEvidence(db, { + residentIds: residents.map((r) => r.id), + staffId: seedStaff?.id ?? null, + }) + console.log( + `✅ Created ${integration.records} learning records, ` + + `${integration.careAssignments} care assignments`, + ) + + const opportunities = await seedOpportunities(db, { + residentIds: residents.map((r) => r.id), + staffId: seedStaff?.id ?? null, + }) + console.log( + `✅ Created ${opportunities.opportunities} opportunities, ` + + `${opportunities.applications} applications, ` + + `${opportunities.evidenceRecords} generated records`, + ) + + // Create placements with CALCULATED compatibility scores + const now = new Date() + const DAY = 24 * 60 * 60 * 1000 + + // Calculate real compatibility scores between roommates + // ZH-001: RES-001 (index 0), RES-002 (index 1), RES-022 (index 21) + const score_0_1 = calculateScore(residents[0], residents[1]) + const score_0_21 = calculateScore(residents[0], residents[21]) + const score_1_21 = calculateScore(residents[1], residents[21]) + console.log(` 📊 RES-001 + RES-002 compatibility: ${score_0_1.compatibilityScore}%`) + console.log(` 📊 RES-001 + RES-022 compatibility: ${score_0_21.compatibilityScore}%`) + + // ZH-002: RES-003 (index 2) with RES-004 (index 3) and RES-006 (index 5) + const score_2_3 = calculateScore(residents[2], residents[3]) + const score_2_5 = calculateScore(residents[2], residents[5]) + const score_3_5 = calculateScore(residents[3], residents[5]) + + // ZH-003: RES-005 (index 4) and RES-007 (index 6) + const score_4_6 = calculateScore(residents[4], residents[6]) + + // ZH-004: RES-014 (13) + RES-015 (14), RES-018 (17) + RES-019 (18) + const score_13_14 = calculateScore(residents[13], residents[14]) + const score_17_18 = calculateScore(residents[17], residents[18]) + console.log(` 📊 RES-014 + RES-015 compatibility: ${score_13_14.compatibilityScore}%`) + console.log(` 📊 RES-018 + RES-019 compatibility: ${score_17_18.compatibilityScore}%`) + + // ZH-006: RES-016 (15) + RES-017 (16) — tension pair + const score_15_16 = calculateScore(residents[15], residents[16]) + console.log(` 📊 RES-016 + RES-017 compatibility: ${score_15_16.compatibilityScore}%`) + + // Ended placement scores + const score_19_17 = calculateScore(residents[19], residents[17]) // RES-020 was with RES-018 in ZH-004 + + // ========================================================================= + // ACTIVE PLACEMENTS + // ========================================================================= + + const placements = await Promise.all([ + // ZH-001: RES-001 and RES-002 (some tension), RES-022 (success story) + createRow(placement, { + residentId: residents[0].id, + housingUnitId: units[0].id, + spotId: allSpots.zh001.beds[0].id, // R1-B1 + startDate: new Date(now.getTime() - 60 * DAY), + compatibilityScore: score_0_1.compatibilityScore, + lifestyleScore: score_0_1.lifestyleScore, + socialScore: score_0_1.socialScore, + practicalScore: score_0_1.practicalScore, + riskScore: score_0_1.riskScore, + status: 'ACTIVE', + placementNotes: + score_0_1.concerns.length > 0 + ? score_0_1.concerns.join('. ') + : 'Kompatibilitätsprüfung durchgeführt', + }), + createRow(placement, { + residentId: residents[1].id, + housingUnitId: units[0].id, + spotId: allSpots.zh001.beds[1].id, // R1-B2 + startDate: new Date(now.getTime() - 45 * DAY), + compatibilityScore: score_0_1.compatibilityScore, + lifestyleScore: score_0_1.lifestyleScore, + socialScore: score_0_1.socialScore, + practicalScore: score_0_1.practicalScore, + riskScore: score_0_1.riskScore, + status: 'ACTIVE', + placementNotes: + score_0_1.concerns.length > 0 + ? score_0_1.concerns.join('. ') + : 'Kompatibilitätsprüfung durchgeführt', + }), + // [2] ZH-001 R2-B1: RES-022 (success story) + createRow(placement, { + residentId: residents[21].id, + housingUnitId: units[0].id, + spotId: allSpots.zh001.beds[2].id, // R2-B1 + startDate: new Date(now.getTime() - 25 * DAY), + compatibilityScore: score_0_21.compatibilityScore, + lifestyleScore: score_0_21.lifestyleScore, + socialScore: score_0_21.socialScore, + practicalScore: score_0_21.practicalScore, + riskScore: score_0_21.riskScore, + status: 'ACTIVE', + placementNotes: 'Gute Kompatibilität mit bestehenden Bewohnern', + }), + // [3] ZH-002: RES-003 private room, [4] RES-004, [5] RES-006 shared rooms + createRow(placement, { + residentId: residents[2].id, + housingUnitId: units[1].id, + spotId: allSpots.zh002.privateRoom.id, + startDate: new Date(now.getTime() - 90 * DAY), + compatibilityScore: Math.round( + (score_2_3.compatibilityScore + score_2_5.compatibilityScore) / 2, + ), + lifestyleScore: Math.round((score_2_3.lifestyleScore + score_2_5.lifestyleScore) / 2), + socialScore: Math.round((score_2_3.socialScore + score_2_5.socialScore) / 2), + practicalScore: Math.round((score_2_3.practicalScore + score_2_5.practicalScore) / 2), + riskScore: Math.round((score_2_3.riskScore + score_2_5.riskScore) / 2), + status: 'ACTIVE', + placementNotes: 'Einzelzimmer wegen hohem Privatsphärebedürfnis (med. Dok.)', + }), + createRow(placement, { + residentId: residents[3].id, + housingUnitId: units[1].id, + spotId: allSpots.zh002.beds[0].id, + startDate: new Date(now.getTime() - 75 * DAY), + compatibilityScore: score_3_5.compatibilityScore, + lifestyleScore: score_3_5.lifestyleScore, + socialScore: score_3_5.socialScore, + practicalScore: score_3_5.practicalScore, + riskScore: score_3_5.riskScore, + status: 'ACTIVE', + placementNotes: + score_3_5.strengths.length > 0 + ? score_3_5.strengths.join('. ') + : 'Kompatibilitätsprüfung durchgeführt', + }), + createRow(placement, { + residentId: residents[5].id, + housingUnitId: units[1].id, + spotId: allSpots.zh002.beds[1].id, + startDate: new Date(now.getTime() - 120 * DAY), + compatibilityScore: score_3_5.compatibilityScore, + lifestyleScore: score_3_5.lifestyleScore, + socialScore: score_3_5.socialScore, + practicalScore: score_3_5.practicalScore, + riskScore: score_3_5.riskScore, + status: 'ACTIVE', + placementNotes: 'Erdgeschoss wegen Mobilität, CPAP-Gerät', + }), + // [6] ZH-003: RES-005, [7] RES-007 (private rooms) + createRow(placement, { + residentId: residents[4].id, + housingUnitId: units[2].id, + spotId: allSpots.zh003.rooms[0].id, + startDate: new Date(now.getTime() - 30 * DAY), + compatibilityScore: score_4_6.compatibilityScore, + lifestyleScore: score_4_6.lifestyleScore, + socialScore: score_4_6.socialScore, + practicalScore: score_4_6.practicalScore, + riskScore: score_4_6.riskScore, + status: 'ACTIVE', + placementNotes: + score_4_6.strengths.length > 0 + ? score_4_6.strengths.join('. ') + : 'Kompatibilitätsprüfung durchgeführt', + }), + createRow(placement, { + residentId: residents[6].id, + housingUnitId: units[2].id, + spotId: allSpots.zh003.rooms[1].id, + startDate: new Date(now.getTime() - 20 * DAY), + compatibilityScore: score_4_6.compatibilityScore, + lifestyleScore: score_4_6.lifestyleScore, + socialScore: score_4_6.socialScore, + practicalScore: score_4_6.practicalScore, + riskScore: score_4_6.riskScore, + status: 'ACTIVE', + placementNotes: + score_4_6.strengths.length > 0 + ? score_4_6.strengths.join('. ') + : 'Kompatibilitätsprüfung durchgeführt', + }), + // [8] ZH-004: RES-014 + RES-015 (good match, R1) + createRow(placement, { + residentId: residents[13].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[0].id, // R1-B1 + startDate: new Date(now.getTime() - 35 * DAY), + compatibilityScore: score_13_14.compatibilityScore, + lifestyleScore: score_13_14.lifestyleScore, + socialScore: score_13_14.socialScore, + practicalScore: score_13_14.practicalScore, + riskScore: score_13_14.riskScore, + status: 'ACTIVE', + placementNotes: 'Gutes Match - ähnlicher Lebensstil', + }), + // [9] + createRow(placement, { + residentId: residents[14].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[1].id, // R1-B2 + startDate: new Date(now.getTime() - 30 * DAY), + compatibilityScore: score_13_14.compatibilityScore, + lifestyleScore: score_13_14.lifestyleScore, + socialScore: score_13_14.socialScore, + practicalScore: score_13_14.practicalScore, + riskScore: score_13_14.riskScore, + status: 'ACTIVE', + placementNotes: 'Gutes Match mit RES-014', + }), + // [10] ZH-004: RES-018 + RES-019 (stable, R3) + createRow(placement, { + residentId: residents[17].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[4].id, // R3-B1 + startDate: new Date(now.getTime() - 50 * DAY), + compatibilityScore: score_17_18.compatibilityScore, + lifestyleScore: score_17_18.lifestyleScore, + socialScore: score_17_18.socialScore, + practicalScore: score_17_18.practicalScore, + riskScore: score_17_18.riskScore, + status: 'ACTIVE', + placementNotes: 'Stabile Platzierung', + }), + // [11] + createRow(placement, { + residentId: residents[18].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[5].id, // R3-B2 + startDate: new Date(now.getTime() - 45 * DAY), + compatibilityScore: score_17_18.compatibilityScore, + lifestyleScore: score_17_18.lifestyleScore, + socialScore: score_17_18.socialScore, + practicalScore: score_17_18.practicalScore, + riskScore: score_17_18.riskScore, + status: 'ACTIVE', + placementNotes: 'Spricht Deutsch, hilft bei Verständigung', + }), + // [12] ZH-006: RES-016 + RES-017 (tension pair, R1) + createRow(placement, { + residentId: residents[15].id, + housingUnitId: units[5].id, + spotId: allSpots.zh006.beds[0].id, // R1-B1 + startDate: new Date(now.getTime() - 28 * DAY), + compatibilityScore: score_15_16.compatibilityScore, + lifestyleScore: score_15_16.lifestyleScore, + socialScore: score_15_16.socialScore, + practicalScore: score_15_16.practicalScore, + riskScore: score_15_16.riskScore, + status: 'ACTIVE', + placementNotes: + score_15_16.concerns.length > 0 + ? score_15_16.concerns.join('. ') + : 'Spannungspotential erkannt', + }), + // [13] + createRow(placement, { + residentId: residents[16].id, + housingUnitId: units[5].id, + spotId: allSpots.zh006.beds[1].id, // R1-B2 + startDate: new Date(now.getTime() - 28 * DAY), + compatibilityScore: score_15_16.compatibilityScore, + lifestyleScore: score_15_16.lifestyleScore, + socialScore: score_15_16.socialScore, + practicalScore: score_15_16.practicalScore, + riskScore: score_15_16.riskScore, + status: 'ACTIVE', + placementNotes: + score_15_16.concerns.length > 0 + ? score_15_16.concerns.join('. ') + : 'Spannungspotential erkannt', + }), + // [14] ZH-006: RES-024 (transferred here from ZH-001, R2) + createRow(placement, { + residentId: residents[23].id, + housingUnitId: units[5].id, + spotId: allSpots.zh006.beds[2].id, // R2-B1 + startDate: new Date(now.getTime() - 15 * DAY), + compatibilityScore: 72, + lifestyleScore: 78, + socialScore: 70, + practicalScore: 75, + riskScore: 15, + status: 'ACTIVE', + placementNotes: 'Versetzung aus ZH-001 zu besserem Match', + }), + ]) + + // ========================================================================= + // ENDED PLACEMENTS (transfer/conflict stories) + // ========================================================================= + + const endedPlacements = await Promise.all([ + // RES-020 was in ZH-004 for 40 days, ended with CONFLICT + createRow(placement, { + residentId: residents[19].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[2].id, // R2-B1 + startDate: new Date(now.getTime() - 80 * DAY), + endDate: new Date(now.getTime() - 40 * DAY), + compatibilityScore: score_19_17.compatibilityScore, + lifestyleScore: score_19_17.lifestyleScore, + socialScore: score_19_17.socialScore, + practicalScore: score_19_17.practicalScore, + riskScore: score_19_17.riskScore, + status: 'ENDED', + endReason: 'CONFLICT', + satisfactionRating: 2, + placementNotes: 'Niedrige Kompatibilität, Nachteulen-Problematik', + outcomeNotes: 'Mehrere Konflikte, Versetzung notwendig', + conflictGap: 'NOISE', + wasPredictable: true, + }), + // RES-024 was in ZH-001 for 20 days, ended with UPGRADE + createRow(placement, { + residentId: residents[23].id, + housingUnitId: units[0].id, + spotId: allSpots.zh001.beds[3].id, // R2-B2 + startDate: new Date(now.getTime() - 50 * DAY), + endDate: new Date(now.getTime() - 15 * DAY), + compatibilityScore: score_0_21.compatibilityScore, + lifestyleScore: score_0_21.lifestyleScore, + socialScore: score_0_21.socialScore, + practicalScore: score_0_21.practicalScore, + riskScore: score_0_21.riskScore, + status: 'ENDED', + endReason: 'UPGRADE', + satisfactionRating: 3, + placementNotes: 'Initiale Platzierung, besseres Match gefunden', + outcomeNotes: 'Versetzt nach ZH-006 für bessere Kompatibilität', + }), + // RES-004 historical: was in ZH-004, natural exit before current placement + createRow(placement, { + residentId: residents[3].id, + housingUnitId: units[3].id, + spotId: allSpots.zh004.beds[6].id, // R4-B1 + startDate: new Date(now.getTime() - 200 * DAY), + endDate: new Date(now.getTime() - 100 * DAY), + compatibilityScore: 65, + lifestyleScore: 70, + socialScore: 60, + practicalScore: 68, + riskScore: 20, + status: 'ENDED', + endReason: 'NATURAL', + satisfactionRating: 4, + placementNotes: 'Erste Platzierung im System', + outcomeNotes: 'Bewohner vorübergehend aus System ausgetreten', + }), + ]) + + console.log(`✅ Created ${placements.length} active + ${endedPlacements.length} ended placements`) + + // Update spot statuses to OCCUPIED for spots with active placements + await Promise.all([ + // ZH-001 + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh001.beds[0].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh001.beds[1].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh001.beds[2].id)), + // ZH-002 + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh002.privateRoom.id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh002.beds[0].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh002.beds[1].id)), + // ZH-003 + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh003.rooms[0].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh003.rooms[1].id)), + // ZH-004 + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh004.beds[0].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh004.beds[1].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh004.beds[4].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh004.beds[5].id)), + // ZH-006 + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh006.beds[0].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh006.beds[1].id)), + db + .update(placementSpot) + .set({ status: 'OCCUPIED' }) + .where(eq(placementSpot.id, allSpots.zh006.beds[2].id)), + ]) + + // ========================================================================= + // COMPATIBILITY ASSESSMENTS + // ========================================================================= + + const assessments = await Promise.all([ + // ZH-001 roommates + createRow(compatibilityAssessment, { + residentId: residents[0].id, + comparedWithId: residents[1].id, + overallScore: score_0_1.compatibilityScore, + lifestyleScore: score_0_1.lifestyleScore, + socialScore: score_0_1.socialScore, + practicalScore: score_0_1.practicalScore, + riskScore: score_0_1.riskScore, + strengths: score_0_1.strengths, + concerns: score_0_1.concerns, + recommendations: score_0_1.recommendations, + }), + createRow(compatibilityAssessment, { + residentId: residents[0].id, + comparedWithId: residents[21].id, + overallScore: score_0_21.compatibilityScore, + lifestyleScore: score_0_21.lifestyleScore, + socialScore: score_0_21.socialScore, + practicalScore: score_0_21.practicalScore, + riskScore: score_0_21.riskScore, + strengths: score_0_21.strengths, + concerns: score_0_21.concerns, + recommendations: score_0_21.recommendations, + }), + createRow(compatibilityAssessment, { + residentId: residents[1].id, + comparedWithId: residents[21].id, + overallScore: score_1_21.compatibilityScore, + lifestyleScore: score_1_21.lifestyleScore, + socialScore: score_1_21.socialScore, + practicalScore: score_1_21.practicalScore, + riskScore: score_1_21.riskScore, + strengths: score_1_21.strengths, + concerns: score_1_21.concerns, + recommendations: score_1_21.recommendations, + }), + // ZH-002 + createRow(compatibilityAssessment, { + residentId: residents[2].id, + comparedWithId: residents[3].id, + overallScore: score_2_3.compatibilityScore, + lifestyleScore: score_2_3.lifestyleScore, + socialScore: score_2_3.socialScore, + practicalScore: score_2_3.practicalScore, + riskScore: score_2_3.riskScore, + strengths: score_2_3.strengths, + concerns: score_2_3.concerns, + recommendations: score_2_3.recommendations, + }), + createRow(compatibilityAssessment, { + residentId: residents[2].id, + comparedWithId: residents[5].id, + overallScore: score_2_5.compatibilityScore, + lifestyleScore: score_2_5.lifestyleScore, + socialScore: score_2_5.socialScore, + practicalScore: score_2_5.practicalScore, + riskScore: score_2_5.riskScore, + strengths: score_2_5.strengths, + concerns: score_2_5.concerns, + recommendations: score_2_5.recommendations, + }), + createRow(compatibilityAssessment, { + residentId: residents[3].id, + comparedWithId: residents[5].id, + overallScore: score_3_5.compatibilityScore, + lifestyleScore: score_3_5.lifestyleScore, + socialScore: score_3_5.socialScore, + practicalScore: score_3_5.practicalScore, + riskScore: score_3_5.riskScore, + strengths: score_3_5.strengths, + concerns: score_3_5.concerns, + recommendations: score_3_5.recommendations, + }), + // ZH-003 + createRow(compatibilityAssessment, { + residentId: residents[4].id, + comparedWithId: residents[6].id, + overallScore: score_4_6.compatibilityScore, + lifestyleScore: score_4_6.lifestyleScore, + socialScore: score_4_6.socialScore, + practicalScore: score_4_6.practicalScore, + riskScore: score_4_6.riskScore, + strengths: score_4_6.strengths, + concerns: score_4_6.concerns, + recommendations: score_4_6.recommendations, + }), + // ZH-004 + createRow(compatibilityAssessment, { + residentId: residents[13].id, + comparedWithId: residents[14].id, + overallScore: score_13_14.compatibilityScore, + lifestyleScore: score_13_14.lifestyleScore, + socialScore: score_13_14.socialScore, + practicalScore: score_13_14.practicalScore, + riskScore: score_13_14.riskScore, + strengths: score_13_14.strengths, + concerns: score_13_14.concerns, + recommendations: score_13_14.recommendations, + }), + createRow(compatibilityAssessment, { + residentId: residents[17].id, + comparedWithId: residents[18].id, + overallScore: score_17_18.compatibilityScore, + lifestyleScore: score_17_18.lifestyleScore, + socialScore: score_17_18.socialScore, + practicalScore: score_17_18.practicalScore, + riskScore: score_17_18.riskScore, + strengths: score_17_18.strengths, + concerns: score_17_18.concerns, + recommendations: score_17_18.recommendations, + }), + // ZH-006 tension pair + createRow(compatibilityAssessment, { + residentId: residents[15].id, + comparedWithId: residents[16].id, + overallScore: score_15_16.compatibilityScore, + lifestyleScore: score_15_16.lifestyleScore, + socialScore: score_15_16.socialScore, + practicalScore: score_15_16.practicalScore, + riskScore: score_15_16.riskScore, + strengths: score_15_16.strengths, + concerns: score_15_16.concerns, + recommendations: score_15_16.recommendations, + }), + ]) + + console.log(`✅ Created ${assessments.length} compatibility assessments`) + + // ========================================================================= + // SATISFACTION CHECK-INS + // ========================================================================= + + const checkIns = await Promise.all([ + // ZH-001 tension pair: Initial (low) → Regular (improving) → Recent (stable) + createRow(satisfactionCheckIn, { + placementId: placements[0].id, // RES-001 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 3, + roommateRelations: 2, + facilitySatisfaction: 4, + safetyFeeling: 4, + concerns: 'Mitbewohner ist laut abends', + collectedBy: 'Frau Müller', + }), + createRow(satisfactionCheckIn, { + placementId: placements[0].id, + checkInType: 'REGULAR', + weekNumber: 4, + overallSatisfaction: 3, + roommateRelations: 3, + facilitySatisfaction: 4, + safetyFeeling: 4, + positives: 'Kopfhörer-Regelung hilft', + collectedBy: 'Frau Müller', + }), + createRow(satisfactionCheckIn, { + placementId: placements[0].id, + checkInType: 'REGULAR', + weekNumber: 8, + overallSatisfaction: 4, + roommateRelations: 3, + facilitySatisfaction: 4, + safetyFeeling: 5, + positives: 'Situation hat sich stabilisiert', + collectedBy: 'Frau Müller', + }), + // ZH-002: Consistently high + createRow(satisfactionCheckIn, { + placementId: placements[4].id, // RES-004 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 4, + roommateRelations: 4, + facilitySatisfaction: 5, + safetyFeeling: 5, + positives: 'Ruhige Umgebung, nette Mitbewohner', + collectedBy: 'Herr Schmidt', + }), + createRow(satisfactionCheckIn, { + placementId: placements[4].id, + checkInType: 'REGULAR', + weekNumber: 6, + overallSatisfaction: 5, + roommateRelations: 5, + facilitySatisfaction: 4, + safetyFeeling: 5, + positives: 'Fühle mich wohl hier', + collectedBy: 'Herr Schmidt', + }), + // ZH-003: High satisfaction (private rooms) + createRow(satisfactionCheckIn, { + placementId: placements[6].id, // RES-005 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 5, + roommateRelations: 4, + facilitySatisfaction: 5, + safetyFeeling: 5, + positives: 'Eigenes Zimmer ist sehr wichtig für mich', + collectedBy: 'Frau Müller', + }), + createRow(satisfactionCheckIn, { + placementId: placements[7].id, // RES-007 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 4, + roommateRelations: 4, + facilitySatisfaction: 5, + safetyFeeling: 5, + positives: 'Ruhige Lage am See', + collectedBy: 'Frau Müller', + }), + // ZH-004: Mixed — good for stable pair, flagged for departed RES-020 + createRow(satisfactionCheckIn, { + placementId: placements[8].id, // RES-014 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 4, + roommateRelations: 5, + facilitySatisfaction: 3, + safetyFeeling: 4, + positives: 'Mitbewohner ist super nett', + collectedBy: 'Herr Schmidt', + }), + createRow(satisfactionCheckIn, { + placementId: endedPlacements[0].id, // RES-020 (ended) + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 2, + roommateRelations: 2, + facilitySatisfaction: 3, + safetyFeeling: 3, + concerns: 'Kann nachts nicht schlafen wegen Mitbewohner', + collectedBy: 'Herr Schmidt', + }), + // ZH-006: Tension pair check-ins + createRow(satisfactionCheckIn, { + placementId: placements[12].id, // RES-016 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 3, + roommateRelations: 2, + facilitySatisfaction: 4, + safetyFeeling: 4, + concerns: 'Mitbewohnerin ist zu laut am Morgen', + collectedBy: 'Frau Müller', + }), + createRow(satisfactionCheckIn, { + placementId: placements[13].id, // RES-017 + checkInType: 'INITIAL', + weekNumber: 1, + overallSatisfaction: 3, + roommateRelations: 3, + facilitySatisfaction: 4, + safetyFeeling: 4, + concerns: 'Mitbewohnerin geht sehr spät ins Bett, Licht stört', + collectedBy: 'Frau Müller', + }), + ]) + + console.log(`✅ Created ${checkIns.length} satisfaction check-ins`) + + // ========================================================================= + // INCIDENTS (6 existing + 6 new = 12) + // ========================================================================= + + const incidents = await Promise.all([ + // --- Existing 6 incidents --- + createRow(incident, { + housingUnitId: units[0].id, + placementId: placements[0].id, + reportedById: residents[0].id, + subjectId: residents[1].id, + date: new Date(now.getTime() - 10 * DAY), + category: 'INTERPERSONAL', + type: 'NOISE_COMPLAINT', + severity: 'MEDIUM', + description: 'RES-001 beschwert sich über laute Musik nach 23 Uhr von RES-002', + resolution: 'Gespräch geführt, Kopfhörer-Regelung vereinbart', + resolvedAt: new Date(now.getTime() - 8 * DAY), + predictable: true, + compatibilityGap: 'lifestyle', + }), + createRow(incident, { + housingUnitId: units[0].id, + placementId: placements[1].id, + subjectId: residents[1].id, + date: new Date(now.getTime() - 3 * DAY), + category: 'INTERPERSONAL', + type: 'SCHEDULE_CONFLICT', + severity: 'LOW', + description: 'Diskussion über Badezimmernutzung am Morgen', + predictable: true, + compatibilityGap: 'lifestyle', + }), + createRow(incident, { + housingUnitId: units[1].id, + date: new Date(now.getTime() - 5 * DAY), + category: 'MAINTENANCE', + type: 'PLUMBING', + severity: 'MEDIUM', + description: 'Wasserhahn in der Küche tropft', + }), + createRow(incident, { + housingUnitId: units[1].id, + date: new Date(now.getTime() - 2 * DAY), + category: 'MAINTENANCE', + type: 'HEATING_COOLING', + severity: 'HIGH', + description: 'Heizung im Zimmer von RES-006 funktioniert nicht richtig', + }), + createRow(incident, { + housingUnitId: units[2].id, + date: new Date(now.getTime() - 14 * DAY), + category: 'MAINTENANCE', + type: 'ELECTRICAL', + severity: 'LOW', + description: 'Lampe im Flur defekt', + resolution: 'Leuchtmittel ersetzt', + resolvedAt: new Date(now.getTime() - 12 * DAY), + }), + createRow(incident, { + housingUnitId: units[3].id, + date: new Date(now.getTime() - 60 * DAY), + category: 'MAINTENANCE', + type: 'APPLIANCE', + severity: 'MEDIUM', + description: 'Kühlschrank macht laute Geräusche', + resolution: 'Neuer Kühlschrank installiert', + resolvedAt: new Date(now.getTime() - 55 * DAY), + }), + // --- 6 new incidents --- + // ZH-006: Interpersonal conflict between tension pair (correlates with low compatibility) + createRow(incident, { + housingUnitId: units[5].id, + placementId: placements[12].id, + reportedById: residents[15].id, // RES-016 + subjectId: residents[16].id, // RES-017 + date: new Date(now.getTime() - 18 * DAY), + category: 'INTERPERSONAL', + type: 'NOISE_COMPLAINT', + severity: 'MEDIUM', + description: 'RES-016 beschwert sich: RES-017 macht morgens um 5:30 Uhr Lärm in der Küche', + resolution: 'Vermittlungsgespräch, Küchenzeiten vereinbart', + resolvedAt: new Date(now.getTime() - 16 * DAY), + predictable: true, + compatibilityGap: 'lifestyle', + followUpPriority: 'NORMAL', + nextFollowUpDate: new Date(now.getTime() + 3 * DAY), + }), + // ZH-006: Second incident — same tension pair, cleanliness + createRow(incident, { + housingUnitId: units[5].id, + placementId: placements[13].id, + reportedById: residents[15].id, // RES-016 + subjectId: residents[16].id, // RES-017 + date: new Date(now.getTime() - 7 * DAY), + category: 'INTERPERSONAL', + type: 'CLEANLINESS_DISPUTE', + severity: 'LOW', + description: 'Streit über Küchensauberkeit, RES-016 empfindet RES-017 als unordentlich', + predictable: true, + compatibilityGap: 'lifestyle', + }), + // ZH-004: Safety concern + createRow(incident, { + housingUnitId: units[3].id, + date: new Date(now.getTime() - 12 * DAY), + category: 'SAFETY', + type: 'SAFETY_CONCERN', + severity: 'HIGH', + description: 'Eingangstür nachts nicht abgeschlossen vorgefunden', + resolution: 'Bewohner erinnert, automatischer Türschliesser wird installiert', + resolvedAt: new Date(now.getTime() - 10 * DAY), + followUpPriority: 'HIGH', + nextFollowUpDate: new Date(now.getTime() - 5 * DAY), + }), + // ZH-004: Conflict involving RES-020 before transfer (resolved) + createRow(incident, { + housingUnitId: units[3].id, + placementId: endedPlacements[0].id, + reportedById: residents[17].id, // RES-018 + subjectId: residents[19].id, // RES-020 + date: new Date(now.getTime() - 50 * DAY), + category: 'INTERPERSONAL', + type: 'NOISE_COMPLAINT', + severity: 'HIGH', + description: 'RES-020 stört nachts Mitbewohner durch lautes Telefonieren', + resolution: 'Versetzung von RES-020 eingeleitet', + resolvedAt: new Date(now.getTime() - 40 * DAY), + predictable: true, + compatibilityGap: 'lifestyle', + }), + // ZH-004: Second conflict with RES-020 + createRow(incident, { + housingUnitId: units[3].id, + placementId: endedPlacements[0].id, + subjectId: residents[19].id, // RES-020 + date: new Date(now.getTime() - 45 * DAY), + category: 'INTERPERSONAL', + type: 'PERSONAL_CONFLICT', + severity: 'MEDIUM', + description: 'Lautstärker Streit zwischen RES-020 und Mitbewohnern über Gemeinschaftsräume', + resolution: 'Vermittlungsgespräch, Versetzungsentscheid bestätigt', + resolvedAt: new Date(now.getTime() - 42 * DAY), + predictable: true, + compatibilityGap: 'social', + }), + // ZH-001: Cultural friction (resolved positively — shows system tracking outcomes) + createRow(incident, { + housingUnitId: units[0].id, + placementId: placements[2].id, + reportedById: residents[21].id, // RES-022 + date: new Date(now.getTime() - 15 * DAY), + category: 'INTERPERSONAL', + type: 'CULTURAL_FRICTION', + severity: 'LOW', + description: 'Missverständnis über Küchennutzung wegen unterschiedlicher Gewohnheiten', + resolution: 'Küchenplan erstellt, Situation geklärt durch gemeinsames Kochen', + resolvedAt: new Date(now.getTime() - 13 * DAY), + predictable: false, + compatibilityGap: 'practical', + }), + ]) + + console.log(`✅ Created ${incidents.length} incidents`) + + // Incident follow-ups + const followUps = await Promise.all([ + // Follow-up on ZH-006 noise complaint (incident index 6) + createRow(incidentFollowUp, { + incidentId: incidents[6].id, + action: 'Nachgespräch mit beiden Bewohnerinnen', + notes: 'Küchenzeiten werden eingehalten, Situation leicht verbessert', + outcome: 'Teilweise Verbesserung', + staffName: 'Frau Müller', + scheduledNextDate: new Date(now.getTime() + 7 * DAY), + }), + // Follow-up on ZH-004 safety concern (incident index 8) + createRow(incidentFollowUp, { + incidentId: incidents[8].id, + action: 'Türschliesser installiert, alle Bewohner informiert', + notes: 'Automatischer Türschliesser funktioniert', + outcome: 'Problem behoben', + staffName: 'Herr Schmidt', + }), + // Follow-up on RES-020 conflict (incident index 9) + createRow(incidentFollowUp, { + incidentId: incidents[9].id, + action: 'Versetzungsgespräch mit RES-020 durchgeführt', + notes: 'Bewohner versteht Gründe, akzeptiert Versetzung', + outcome: 'Versetzung durchgeführt', + staffName: 'Herr Schmidt', + }), + ]) + + console.log(`✅ Created ${followUps.length} incident follow-ups`) + + // ========================================================================= + // MAINTENANCE REQUESTS + // ========================================================================= + + const maintenanceRequests = await Promise.all([ + // Open: plumbing issue in ZH-006 + createRow(maintenanceRequest, { + housingUnitId: units[5].id, + category: 'PLUMBING', + priority: 'NORMAL', + title: 'Dusche tropft', + description: 'Duschkopf in Badezimmer 1 tropft kontinuierlich', + location: 'Badezimmer 1', + reportedById: residents[15].id, + status: 'OPEN', + }), + // Open: heating repair in ZH-002 + createRow(maintenanceRequest, { + housingUnitId: units[1].id, + category: 'HEATING_COOLING', + priority: 'HIGH', + title: 'Heizung Zimmer 2 defekt', + description: 'Heizung heizt nicht mehr richtig, Zimmer wird kalt', + location: 'Zimmer 2', + reportedById: residents[5].id, + status: 'ASSIGNED', + assignedTo: 'Hauswart Keller', + assignedAt: new Date(now.getTime() - 1 * DAY), + }), + // Completed: electrical fix in ZH-003 + createRow(maintenanceRequest, { + housingUnitId: units[2].id, + category: 'ELECTRICAL', + priority: 'NORMAL', + title: 'Steckdose funktioniert nicht', + description: 'Steckdose neben Bett in Einzelzimmer 1 ohne Strom', + location: 'Einzelzimmer 1', + reportedById: residents[4].id, + status: 'COMPLETED', + assignedTo: 'Elektriker Meyer', + assignedAt: new Date(now.getTime() - 10 * DAY), + startedAt: new Date(now.getTime() - 8 * DAY), + completedAt: new Date(now.getTime() - 7 * DAY), + resolution: 'Sicherung war defekt, ersetzt', + cost: 85.0, + }), + // Completed: appliance replacement in ZH-004 + createRow(maintenanceRequest, { + housingUnitId: units[3].id, + category: 'APPLIANCE', + priority: 'NORMAL', + title: 'Waschmaschine defekt', + description: 'Waschmaschine schleudert nicht mehr richtig', + location: 'Waschküche', + reporterName: 'Hauswart Keller', + status: 'COMPLETED', + assignedTo: 'Electrolux Service', + assignedAt: new Date(now.getTime() - 20 * DAY), + startedAt: new Date(now.getTime() - 15 * DAY), + completedAt: new Date(now.getTime() - 12 * DAY), + resolution: 'Neue Waschmaschine installiert', + cost: 890.0, + }), + ]) + + console.log(`✅ Created ${maintenanceRequests.length} maintenance requests`) + + // ========================================================================= + // TRANSFER REQUESTS + // ========================================================================= + + const transferRequests = await Promise.all([ + // PENDING: RES-001 wants to move from ZH-001 to ZH-002 (awaiting staff review) + createRow(transferRequest, { + residentId: residents[0].id, + currentPlacementId: placements[0].id, + targetUnitId: units[1].id, + reason: + 'Meine aktuelle Wohngemeinschaft ist sehr laut und ich schlafe schlecht. Ich würde gerne in eine ruhigere Unterkunft wechseln.', + status: 'PENDING', + }), + // APPROVED: RES-004 requested transfer from ZH-002 to ZH-003 (approved 5 days ago) + createRow(transferRequest, { + residentId: residents[3].id, + currentPlacementId: placements[4].id, + targetUnitId: units[2].id, + reason: 'Ich möchte in eine kleinere Unterkunft wechseln, da ich mehr Privatsphäre benötige.', + status: 'APPROVED', + staffNotes: 'Transferanfrage genehmigt. Verlegung wird nächste Woche koordiniert.', + reviewedBy: `${BRAND.codePrefix}ADMIN1`, + reviewedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), + }), + // DENIED: RES-006 requested transfer (denied — no suitable spot available) + createRow(transferRequest, { + residentId: residents[5].id, + currentPlacementId: placements[5].id, + reason: 'Ich möchte näher an meiner Sprachschule wohnen, um den Weg zu verkürzen.', + status: 'DENIED', + staffNotes: + 'Leider kein geeigneter Platz in der gewünschten Lage verfügbar. Bitte in 4 Wochen erneut anfragen.', + reviewedBy: `${BRAND.codePrefix}ADMIN1`, + reviewedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + }), + ]) + + console.log(`✅ Created ${transferRequests.length} transfer requests`) + + // ========================================================================= + // HOUSEHOLD TASKS + // ========================================================================= + + const householdTasks = await Promise.all([ + // Kitchen cleaning schedule (recurring) in ZH-001 + createRow(householdTask, { + housingUnitId: units[0].id, + title: 'Küche reinigen', + description: 'Arbeitsflächen, Herd und Spüle reinigen', + instructions: 'Alle Oberflächen abwischen, Herd reinigen, Spüle putzen, Boden wischen', + taskType: 'RECURRING_SCHEDULED', + category: 'CLEANING', + priority: 'NORMAL', + scheduleHuman: 'Jeden Montag und Donnerstag', + estimatedMinutes: 30, + currentStatus: 'IDLE', + createdByStaff: 'Frau Müller', + }), + // Trash duty (recurring) in ZH-004 + createRow(householdTask, { + housingUnitId: units[3].id, + title: 'Müll rausbringen', + description: 'Kehrichtsack und Recycling zur Sammelstelle bringen', + instructions: 'Kehricht (grauer Sack) Dienstag, Papier/Karton Mittwoch, PET jederzeit', + taskType: 'RECURRING_SCHEDULED', + category: 'TRASH', + priority: 'NORMAL', + scheduleHuman: 'Jeden Dienstag (Kehricht), Mittwoch (Papier)', + estimatedMinutes: 15, + currentStatus: 'IDLE', + createdByStaff: 'Herr Schmidt', + }), + // One-time deep clean in ZH-006 + createRow(householdTask, { + housingUnitId: units[5].id, + title: 'Grundreinigung Badezimmer', + description: 'Gründliche Reinigung beider Badezimmer', + instructions: 'Fliesen, Fugen, WC, Dusche, Waschbecken, Spiegel reinigen', + taskType: 'ONE_TIME', + category: 'CLEANING', + priority: 'HIGH', + estimatedMinutes: 60, + currentStatus: 'NEEDS_ATTENTION', + createdByStaff: 'Frau Müller', + }), + // Kitchen cleaning (recurring) in ZH-002 + createRow(householdTask, { + housingUnitId: units[1].id, + title: 'Küche und Gemeinschaftsräume', + description: 'Wöchentliche Reinigung der Gemeinschaftsräume', + instructions: 'Küche reinigen, Wohnbereich staubsaugen, Oberflächen abwischen', + taskType: 'RECURRING_SCHEDULED', + category: 'CLEANING', + priority: 'NORMAL', + scheduleHuman: 'Jeden Freitag', + estimatedMinutes: 45, + currentStatus: 'IDLE', + createdByStaff: 'Herr Schmidt', + }), + ]) + + console.log(`✅ Created ${householdTasks.length} household tasks`) + + // ========================================================================= + // UPDATE UNIT STATUSES + // ========================================================================= + + // ZH-001: 3/4 beds occupied + // ZH-002: 3/6 beds occupied + // ZH-003: 2/3 rooms occupied + // ZH-004: 4/8 beds occupied + // ZH-005: maintenance + // ZH-006: 3/6 beds occupied + // ZH-007: empty, available + // ZH-008: empty, available + + // Pilot baseline config (from CLAUDE.md example metrics — pre-system Phase-1 data) + const earliestPlacement = await db.query.placement.findFirst({ + orderBy: [asc(placement.startDate)], + columns: { startDate: true }, + }) + await db + .insert(systemConfig) + .values({ + id: 'singleton', + pilotBaselineIncidentsPerMonth: 15, + pilotBaselineRelocationsPerMonth: 4, + pilotBaselineMediationHoursPerWeek: 12, + pilotStartDate: earliestPlacement?.startDate ?? new Date('2025-11-01'), + }) + .onConflictDoUpdate({ + target: systemConfig.id, + set: { + pilotBaselineIncidentsPerMonth: 15, + pilotBaselineRelocationsPerMonth: 4, + pilotBaselineMediationHoursPerWeek: 12, + pilotStartDate: earliestPlacement?.startDate ?? new Date('2025-11-01'), + }, + }) + + // --------------------------------------------------------------------------- + // GOVERNANCE — AOZ rule catalog + a house that has started using it + // --------------------------------------------------------------------------- + + // The AOZ tier is reference data, not demo data: it must exist in production + // too. Idempotent, so re-seeding never duplicates or resets acknowledgements. + const ruleSync = await syncOrgRules(db) + + const quietRule = await db.query.houseRule.findFirst({ where: eq(houseRule.key, 'night_quiet') }) + const kitchenRule = await db.query.houseRule.findFirst({ + where: eq(houseRule.key, 'kitchen_use'), + }) + const cleaningRule = await db.query.houseRule.findFirst({ + where: eq(houseRule.key, 'shared_cleaning'), + }) + + const demoUnit = units[0] + let houseRuleCount = 0 + let proposalCount = 0 + + if (demoUnit && kitchenRule && quietRule && cleaningRule) { + // A house that has already decided one topic... + const existingHouseRule = await db.query.houseRule.findFirst({ + where: and( + eq(houseRule.scope, 'UNIT'), + eq(houseRule.housingUnitId, demoUnit.id), + eq(houseRule.parentRuleId, kitchenRule.id), + ), + }) + + if (!existingHouseRule) { + await createRow(houseRule, { + scope: 'UNIT', + housingUnitId: demoUnit.id, + parentRuleId: kitchenRule.id, + category: kitchenRule.category, + title: 'Küche: abwaschen am selben Abend', + body: 'Wer kocht, wäscht am selben Abend ab. Geschirr, das über Nacht stehen bleibt, wird in eine Kiste neben der Spüle geräumt. Am Sonntag räumt die Person auf, die in der Woche dran war.', + delegation: kitchenRule.delegation, + status: 'ACTIVE', + version: 1, + }) + houseRuleCount++ + } + + // ...and one still being decided, so the voting UI has something to show. + const demoUnitResidents = await db.query.placement.findMany({ + where: and(eq(placement.housingUnitId, demoUnit.id), eq(placement.status, 'ACTIVE')), + columns: { residentId: true }, + }) + const voterIds = Array.from(new Set(demoUnitResidents.map((p) => p.residentId))) + + const existingProposal = await db.query.proposal.findFirst({ + where: eq(proposalTable.housingUnitId, demoUnit.id), + }) + + if (!existingProposal && voterIds.length >= 3) { + const votingEndsAt = new Date() + votingEndsAt.setDate(votingEndsAt.getDate() + 4) + + const proposal = await createRow(proposalTable, { + housingUnitId: demoUnit.id, + type: 'ADD_RULE', + category: cleaningRule.category, + title: 'Putzplan mit fixem Wochentag', + body: 'Vorschlag: Jede Person übernimmt eine Woche lang Küche und Bad. Der Wechsel ist immer am Sonntagabend. Wer in seiner Woche verhindert ist, tauscht vorher mit jemandem.', + parentOrgRuleId: cleaningRule.id, + proposedByResidentId: voterIds[0], + status: 'VOTING', + decisionMode: 'RESIDENT_BINDING', + threshold: 'SIMPLE_MAJORITY', + quorumPercent: 50, + approvalPercent: 51, + eligibleVoterCount: voterIds.length, + votingOpenedAt: new Date(), + votingEndsAt, + }) + proposalCount++ + + // Enough votes to be interesting but not yet decided. + await db + .insert(vote) + .values([ + { proposalId: proposal.id, residentId: voterIds[0], choice: 'YES' }, + { proposalId: proposal.id, residentId: voterIds[1], choice: 'YES' }, + ]) + .onConflictDoNothing() + } + } + + console.log('✅ Database seeded successfully!') + console.log('') + console.log('📊 Summary:') + console.log(` - ${units.length} housing units`) + const placed = residents.filter((r) => r.status === 'PLACED').length + const active = residents.filter((r) => r.status === 'ACTIVE').length + const transferred = residents.filter((r) => r.status === 'TRANSFERRED').length + console.log( + ` - ${residents.length} residents (${placed} placed, ${active} waiting, ${transferred} transferred)`, + ) + console.log(` - ${placements.length} active + ${endedPlacements.length} ended placements`) + console.log(` - ${assessments.length} compatibility assessments`) + console.log(` - ${checkIns.length} satisfaction check-ins`) + console.log(` - ${incidents.length} incidents (${followUps.length} follow-ups)`) + console.log(` - ${maintenanceRequests.length} maintenance requests`) + console.log(` - ${transferRequests.length} transfer requests (1 pending, 1 approved, 1 denied)`) + console.log(` - ${householdTasks.length} household tasks`) + console.log( + ` - ${ruleSync.created + ruleSync.unchanged + ruleSync.amended} AOZ rules ` + + `(${ruleSync.created} new), ${houseRuleCount} house rule(s), ${proposalCount} open decision(s)`, + ) + console.log('') + console.log('🚀 Ready to run: npm run dev') +} + +main() + .then(() => { + // The pg Pool keeps the event loop alive — exit explicitly on success. + process.exit(0) + }) + .catch((e) => { + console.error('❌ Seed failed:', e) + process.exit(1) + }) diff --git a/scripts/maintenance/ensure-aoz-team.ts b/scripts/maintenance/ensure-aoz-team.ts index ec9d64f7..7bdeaeee 100644 --- a/scripts/maintenance/ensure-aoz-team.ts +++ b/scripts/maintenance/ensure-aoz-team.ts @@ -1,7 +1,7 @@ /** * Provision the real AOZ team, idempotently. * - * Reads `prisma/real/aoz-team.ts` and makes the database match it. Safe to run + * Reads `scripts/db/real/aoz-team.ts` and makes the database match it. Safe to run * repeatedly: an existing person is matched by name and UPDATED to the shape * the config declares, so this doubles as the way to correct someone's reach * after the fact. A code is minted only for someone who does not exist yet, @@ -15,7 +15,7 @@ import { eq } from 'drizzle-orm' import { db, user } from '../../src/lib/db' -import { AOZ_TEAM } from '../../prisma/real/aoz-team' +import { AOZ_TEAM } from '../db/real/aoz-team' import { BRAND } from '../../src/lib/config/brand' import { generateStaffCode } from '../../src/lib/auth/code-generation' import { CARE_ROLES, CARE_ROLE_LABELS, STAFF_ROLE_CARE_DOMAIN } from '../../src/lib/config/care' diff --git a/src/app/api/__tests__/staff-chores.test.ts b/src/app/api/__tests__/staff-chores.test.ts index 9f5355bb..ff5b3f90 100644 --- a/src/app/api/__tests__/staff-chores.test.ts +++ b/src/app/api/__tests__/staff-chores.test.ts @@ -6,12 +6,19 @@ jest.mock('@/lib/auth', () => ({ requireStaffAuth: () => mockRequireStaffAuth(), })) -const mockUnitFindUnique = jest.fn() +const mockUnitFindFirst = jest.fn() const mockTaskCreate = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - housingUnit: { findUnique: (...args: unknown[]) => mockUnitFindUnique(...args) }, - householdTask: { create: (...args: unknown[]) => mockTaskCreate(...args) }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + housingUnit: { findFirst: (...args: unknown[]) => mockUnitFindFirst(...args) }, + }, + insert: jest.fn(() => ({ + values: (v: unknown) => ({ + returning: (): Promise => mockTaskCreate(v), + }), + })), }, })) @@ -45,9 +52,9 @@ const VALID = { beforeEach(() => { jest.clearAllMocks() mockRequireStaffAuth.mockResolvedValue({ id: 'staff-1', name: 'M. Keller', role: 'ADMIN' }) - mockUnitFindUnique.mockResolvedValue({ id: 'unit-1', code: 'DEMO-U05' }) - mockTaskCreate.mockImplementation((args: { data: unknown }) => - Promise.resolve({ id: 'task-1', ...(args.data as object) }), + mockUnitFindFirst.mockResolvedValue({ id: 'unit-1', code: 'DEMO-U05' }) + mockTaskCreate.mockImplementation((values: unknown) => + Promise.resolve([{ id: 'task-1', ...(values as object) }]), ) }) @@ -66,9 +73,7 @@ describe('POST /api/chores (staff)', () => { expect(response.status).toBe(200) expect(mockTaskCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ housingUnitId: 'unit-1', title: 'Küche putzen' }), - }), + expect.objectContaining({ housingUnitId: 'unit-1', title: 'Küche putzen' }), ) }) @@ -80,21 +85,21 @@ describe('POST /api/chores (staff)', () => { await post(VALID) expect(mockTaskCreate).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ createdByStaff: 'M. Keller' }) }), + expect.objectContaining({ createdByStaff: 'M. Keller' }), ) }) it('never attributes a staff task to a resident', async () => { await post(VALID) - const { data } = mockTaskCreate.mock.calls[0][0] - expect(data.createdByResidentId).toBeUndefined() + const [values] = mockTaskCreate.mock.calls[0] + expect(values.createdByResidentId).toBeUndefined() }) it('rejects a unit that does not exist rather than failing on the foreign key', async () => { // An id from a form is an id the caller chose. Without this the failure is // a Prisma FK error and a 500, which tells nobody anything. - mockUnitFindUnique.mockResolvedValue(null) + mockUnitFindFirst.mockResolvedValue(null) const response = await post({ ...VALID, housingUnitId: 'made-up' }) diff --git a/src/app/api/auth/__tests__/auth-routes.test.ts b/src/app/api/auth/__tests__/auth-routes.test.ts index 91315991..79f0e209 100644 --- a/src/app/api/auth/__tests__/auth-routes.test.ts +++ b/src/app/api/auth/__tests__/auth-routes.test.ts @@ -27,14 +27,21 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockUserFindUnique = jest.fn() -const mockUserCreate = jest.fn() +const mockUserFindFirst = jest.fn() +const mockUserInsertReturning = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - user: { - findUnique: (...args: unknown[]) => mockUserFindUnique(...args), - create: (...args: unknown[]) => mockUserCreate(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { + findFirst: (...args: unknown[]) => mockUserFindFirst(...args), + }, }, + insert: jest.fn(() => ({ + values: jest.fn((v: unknown) => ({ + returning: (): Promise => mockUserInsertReturning(v), + })), + })), }, })) @@ -231,16 +238,18 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { }) test('creates staff user with provided code', async () => { - mockUserFindUnique.mockResolvedValue(null) // code not taken + mockUserFindFirst.mockResolvedValue(null) // code not taken // No role in the request, so the new account gets the NARROWEST one. // This previously expected ADMIN, pinning in place the behaviour that made // every one of the 23 staff accounts in production a Leitung. - mockUserCreate.mockResolvedValue({ - id: 'new-1', - code: `${BRAND.codePrefix}CUSTOM`, - name: 'New Staff', - role: 'BETREUUNG', - }) + mockUserInsertReturning.mockResolvedValue([ + { + id: 'new-1', + code: `${BRAND.codePrefix}CUSTOM`, + name: 'New Staff', + role: 'BETREUUNG', + }, + ]) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'New Staff', @@ -256,29 +265,29 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { expect(body.user.name).toBe('New Staff') expect(body.user.role).toBe('BETREUUNG') - expect(mockUserCreate).toHaveBeenCalledWith( + expect(mockUserInsertReturning).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - code: `${BRAND.codePrefix}CUSTOM`, - name: 'New Staff', - role: 'BETREUUNG', - active: true, - }), + code: `${BRAND.codePrefix}CUSTOM`, + name: 'New Staff', + role: 'BETREUUNG', + active: true, }), ) }) test('generates code when none provided', async () => { // First findUnique (code gen check) returns null (code available) - mockUserFindUnique + mockUserFindFirst .mockResolvedValueOnce(null) // generated code is free .mockResolvedValueOnce(null) // uniqueness check passes - mockUserCreate.mockResolvedValue({ - id: 'new-2', - code: `${BRAND.codePrefix}GEN001`, - name: 'Auto Code', - viewer: { role: 'ADMIN', scope: 'ALL_DOMAINS', isSystemAdmin: true }, - }) + mockUserInsertReturning.mockResolvedValue([ + { + id: 'new-2', + code: `${BRAND.codePrefix}GEN001`, + name: 'Auto Code', + role: 'BETREUUNG', + }, + ]) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'Auto Code' }) @@ -292,7 +301,7 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { test('returns 409 when code already exists', async () => { // For provided code: uniqueness check finds existing user - mockUserFindUnique.mockResolvedValue({ id: 'existing' }) + mockUserFindFirst.mockResolvedValue({ id: 'existing' }) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'Duplicate', @@ -308,7 +317,7 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { test('returns 500 when all code generation attempts fail', async () => { // All generated codes collide - mockUserFindUnique.mockResolvedValue({ id: 'existing' }) + mockUserFindFirst.mockResolvedValue({ id: 'existing' }) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'No Code Available', @@ -323,8 +332,8 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { }) test('returns 500 when database create fails', async () => { - mockUserFindUnique.mockResolvedValue(null) // code available - mockUserCreate.mockRejectedValue(new Error('DB error')) + mockUserFindFirst.mockResolvedValue(null) // code available + mockUserInsertReturning.mockRejectedValue(new Error('DB error')) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'DB Fail', @@ -340,13 +349,15 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { }) test('uppercases provided code', async () => { - mockUserFindUnique.mockResolvedValue(null) - mockUserCreate.mockResolvedValue({ - id: 'new-3', - code: `${BRAND.codePrefix}LOWER1`, - name: 'Lower', - viewer: { role: 'ADMIN', scope: 'ALL_DOMAINS', isSystemAdmin: true }, - }) + mockUserFindFirst.mockResolvedValue(null) + mockUserInsertReturning.mockResolvedValue([ + { + id: 'new-3', + code: `${BRAND.codePrefix}LOWER1`, + name: 'Lower', + role: 'BETREUUNG', + }, + ]) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: 'Lower', @@ -357,21 +368,21 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { const body = await res.json() expect(res.status).toBe(200) - expect(mockUserCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ code: `${BRAND.codePrefix}LOWER1` }), - }), + expect(mockUserInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ code: `${BRAND.codePrefix}LOWER1` }), ) }) test('trims name before storing', async () => { - mockUserFindUnique.mockResolvedValue(null) - mockUserCreate.mockResolvedValue({ - id: 'new-4', - code: `${BRAND.codePrefix}TRIM01`, - name: 'Trimmed Name', - viewer: { role: 'ADMIN', scope: 'ALL_DOMAINS', isSystemAdmin: true }, - }) + mockUserFindFirst.mockResolvedValue(null) + mockUserInsertReturning.mockResolvedValue([ + { + id: 'new-4', + code: `${BRAND.codePrefix}TRIM01`, + name: 'Trimmed Name', + role: 'BETREUUNG', + }, + ]) const req = createJsonRequest('http://localhost:3001/api/auth/register', { name: ' Trimmed Name ', @@ -380,10 +391,8 @@ describe('POST /api/auth/register (admin-only staff provisioning)', () => { await registerPOST(req) - expect(mockUserCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ name: 'Trimmed Name' }), - }), + expect(mockUserInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Trimmed Name' }), ) }) }) diff --git a/src/app/api/auth/__tests__/demo.test.ts b/src/app/api/auth/__tests__/demo.test.ts index b394e414..99a9041f 100644 --- a/src/app/api/auth/__tests__/demo.test.ts +++ b/src/app/api/auth/__tests__/demo.test.ts @@ -49,16 +49,20 @@ jest.mock('@/lib/logger', () => ({ // database. Config presence proves nothing now that codes are derived: it // would offer five buttons on an instance where the seed never ran. const mockUserFindMany = jest.fn() -const mockResidentFindUnique = jest.fn() +const mockResidentFindFirst = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - user: { findMany: (...args: unknown[]) => mockUserFindMany(...args) }, - resident: { findUnique: (...args: unknown[]) => mockResidentFindUnique(...args) }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { findMany: (...args: unknown[]) => mockUserFindMany(...args) }, + resident: { findFirst: (...args: unknown[]) => mockResidentFindFirst(...args) }, + }, }, })) // --- Import after mocks --- import { POST, GET } from '../demo/route' +import { demoStaffDoors } from '@/lib/demo/roles' // --- Helpers --- @@ -96,11 +100,13 @@ describe('POST /api/auth/demo', () => { type: 'staff', user: STAFF_USER, }) - // Every staff door's account exists unless a test says otherwise. - mockUserFindMany.mockImplementation(async (args: { where: { code: { in: string[] } } }) => - args.where.code.in.map((code) => ({ code })), + // Every staff door's account exists unless a test says otherwise. The + // where arg is now a drizzle expression, so derive the codes from the same + // source the route does instead of picking the Prisma `in` list apart. + mockUserFindMany.mockImplementation(async () => + demoStaffDoors().map((door) => ({ code: door.code })), ) - mockResidentFindUnique.mockResolvedValue({ id: 'demo-resident-id' }) + mockResidentFindFirst.mockResolvedValue({ id: 'demo-resident-id' }) }) afterEach(() => { @@ -140,7 +146,7 @@ describe('POST /api/auth/demo', () => { // The rule the old version stated and this one keeps: a button appears // only when pressing it can succeed. mockUserFindMany.mockResolvedValue([]) - mockResidentFindUnique.mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const response = await GET() const body = await response.json() diff --git a/src/app/api/auth/__tests__/invite.test.ts b/src/app/api/auth/__tests__/invite.test.ts index a9f4d93b..8bf938b3 100644 --- a/src/app/api/auth/__tests__/invite.test.ts +++ b/src/app/api/auth/__tests__/invite.test.ts @@ -30,21 +30,34 @@ jest.mock('@/lib/auth/code-generation', () => ({ generateStaffCode: jest.fn(() => 'AOZ-GEN001'), })) -const mockUserFindUnique = jest.fn() -const mockUserCreate = jest.fn() -const mockAccountFindUnique = jest.fn() -jest.mock('@/lib/db', () => ({ - prisma: { - user: { - findUnique: (...args: unknown[]) => mockUserFindUnique(...args), - create: (...args: unknown[]) => mockUserCreate(...args), - }, - // Email lives on the Account, not the User. - account: { - findUnique: (...args: unknown[]) => mockAccountFindUnique(...args), +const mockUserFindFirst = jest.fn() +const mockUserInsertReturning = jest.fn() +const mockAccountFindFirst = jest.fn() +const mockAccountInsertReturning = jest.fn() +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + return { + ...actual, + db: { + query: { + user: { findFirst: (...args: unknown[]) => mockUserFindFirst(...args) }, + // Email lives on the Account, not the User. + account: { findFirst: (...args: unknown[]) => mockAccountFindFirst(...args) }, + }, + // The route creates User + Account inside one transaction; dispatch each + // tx.insert to the mock for the table it targets. + transaction: (fn: (tx: unknown) => unknown) => + fn({ + insert: (table: unknown) => ({ + values: (v: unknown) => ({ + returning: (): Promise => + table === actual.user ? mockUserInsertReturning(v) : mockAccountInsertReturning(v), + }), + }), + }), }, - }, -})) + } +}) const mockSendEmail = jest.fn() jest.mock('@/lib/email/service', () => ({ @@ -93,14 +106,12 @@ describe('POST /api/auth/invite', () => { jest.clearAllMocks() mockCheckRateLimit.mockReturnValue({ allowed: true }) mockGetCurrentUser.mockResolvedValue(ADMIN_USER) - mockUserFindUnique.mockResolvedValue(null) // code not taken - mockAccountFindUnique.mockResolvedValue(null) // email not taken - mockUserCreate.mockResolvedValue({ - id: 'new-1', - code: 'AOZ-GEN001', - name: 'New Staff', - account: { email: 'new@aoz.ch' }, - }) + mockUserFindFirst.mockResolvedValue(null) // code not taken + mockAccountFindFirst.mockResolvedValue(null) // email not taken + mockUserInsertReturning.mockResolvedValue([ + { id: 'new-1', code: 'AOZ-GEN001', name: 'New Staff' }, + ]) + mockAccountInsertReturning.mockResolvedValue([{ email: 'new@aoz.ch' }]) mockSendEmail.mockResolvedValue(true) }) @@ -197,7 +208,7 @@ describe('POST /api/auth/invite', () => { // ── Email uniqueness ─────────────────────────────────────────────────────── test('returns 409 when email already registered', async () => { - mockAccountFindUnique.mockResolvedValue({ id: 'existing-1' }) // email already taken + mockAccountFindFirst.mockResolvedValue({ id: 'existing-1' }) // email already taken const req = createJsonRequest({ email: 'existing@aoz.ch', name: 'Duplicate' }) const res = await POST(req) @@ -212,7 +223,7 @@ describe('POST /api/auth/invite', () => { test('returns 500 when all code generation attempts fail', async () => { // All generated codes collide - mockUserFindUnique.mockResolvedValue({ id: 'existing' }) + mockUserFindFirst.mockResolvedValue({ id: 'existing' }) const req = createJsonRequest({ email: 'new@aoz.ch', name: 'New Staff' }) const res = await POST(req) @@ -234,7 +245,7 @@ describe('POST /api/auth/invite', () => { expect(res.status).toBe(403) expect(body.success).toBe(false) - expect(mockUserCreate).not.toHaveBeenCalled() + expect(mockUserInsertReturning).not.toHaveBeenCalled() }) test('creates user with an explicit role', async () => { @@ -242,10 +253,8 @@ describe('POST /api/auth/invite', () => { const res = await POST(req) expect(res.status).toBe(200) - expect(mockUserCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ role: 'JOBCOACH' }), - }), + expect(mockUserInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ role: 'JOBCOACH' }), ) }) @@ -261,17 +270,18 @@ describe('POST /api/auth/invite', () => { expect(body.user.email).toBe('new@aoz.ch') expect(body.emailSent).toBe(true) - expect(mockUserCreate).toHaveBeenCalledWith( + expect(mockUserInsertReturning).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - code: 'AOZ-GEN001', - name: 'New Staff', - account: { create: { email: 'new@aoz.ch' } }, - role: 'BETREUUNG', - active: true, - }), + code: 'AOZ-GEN001', + name: 'New Staff', + role: 'BETREUUNG', + active: true, }), ) + // The Account row is a second insert in the same transaction, linked by FK. + expect(mockAccountInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ email: 'new@aoz.ch', userId: 'new-1' }), + ) expect(mockSendEmail).toHaveBeenCalledWith( ['new@aoz.ch'], expect.any(String), @@ -283,10 +293,8 @@ describe('POST /api/auth/invite', () => { const req = createJsonRequest({ email: 'Upper@AOZ.CH', name: 'Mixed Case' }) await POST(req) - expect(mockUserCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ account: { create: { email: 'upper@aoz.ch' } } }), - }), + expect(mockAccountInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ email: 'upper@aoz.ch' }), ) }) @@ -294,10 +302,8 @@ describe('POST /api/auth/invite', () => { const req = createJsonRequest({ email: 'test@aoz.ch', name: ' Padded Name ' }) await POST(req) - expect(mockUserCreate).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ name: 'Padded Name' }), - }), + expect(mockUserInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ name: 'Padded Name' }), ) }) diff --git a/src/app/api/auth/__tests__/provisioning-least-privilege.test.ts b/src/app/api/auth/__tests__/provisioning-least-privilege.test.ts index a1590006..ec8b0954 100644 --- a/src/app/api/auth/__tests__/provisioning-least-privilege.test.ts +++ b/src/app/api/auth/__tests__/provisioning-least-privilege.test.ts @@ -27,14 +27,21 @@ jest.mock('@/lib/auth/code-generation', () => ({ generateStaffCode: jest.fn(() => 'AOZ-GEN001'), })) -const mockUserFindUnique = jest.fn() -const mockUserCreate = jest.fn() +const mockUserFindFirst = jest.fn() +const mockUserInsertReturning = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - user: { - findUnique: (...args: unknown[]) => mockUserFindUnique(...args), - create: (...args: unknown[]) => mockUserCreate(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { + findFirst: (...args: unknown[]) => mockUserFindFirst(...args), + }, }, + insert: jest.fn(() => ({ + values: jest.fn((v: unknown) => ({ + returning: (): Promise => mockUserInsertReturning(v), + })), + })), }, })) @@ -54,13 +61,15 @@ function post(body: Record): NextRequest { beforeEach(() => { jest.clearAllMocks() - mockUserFindUnique.mockResolvedValue(null) - mockUserCreate.mockResolvedValue({ - id: 'u1', - code: 'AOZ-GEN001', - name: 'Neue Person', - role: 'BETREUUNG', - }) + mockUserFindFirst.mockResolvedValue(null) + mockUserInsertReturning.mockResolvedValue([ + { + id: 'u1', + code: 'AOZ-GEN001', + name: 'Neue Person', + role: 'BETREUUNG', + }, + ]) }) describe('staff provisioning', () => { @@ -76,7 +85,7 @@ describe('staff provisioning', () => { const response = await POST(post({ name: 'Neue Person' })) expect(response.status).toBe(403) - expect(mockUserCreate).not.toHaveBeenCalled() + expect(mockUserInsertReturning).not.toHaveBeenCalled() }) it('refuses an unauthenticated caller', async () => { @@ -85,7 +94,7 @@ describe('staff provisioning', () => { const response = await POST(post({ name: 'Neue Person' })) expect(response.status).toBe(401) - expect(mockUserCreate).not.toHaveBeenCalled() + expect(mockUserInsertReturning).not.toHaveBeenCalled() }) it('gives an unspecified role the NARROWEST role, never Leitung', async () => { @@ -98,10 +107,10 @@ describe('staff provisioning', () => { await POST(post({ name: 'Neue Person' })) - expect(mockUserCreate).toHaveBeenCalledTimes(1) - const [args] = mockUserCreate.mock.calls[0] - expect(args.data.role).toBe('BETREUUNG') - expect(args.data.role).not.toBe('ADMIN') + expect(mockUserInsertReturning).toHaveBeenCalledTimes(1) + const [values] = mockUserInsertReturning.mock.calls[0] + expect(values.role).toBe('BETREUUNG') + expect(values.role).not.toBe('ADMIN') }) it('refuses the retired all-in-one role even when asked for explicitly', async () => { @@ -118,7 +127,7 @@ describe('staff provisioning', () => { const response = await POST(post({ name: 'Neue Person', role: 'ADMIN' })) expect(response.status).toBe(400) - expect(mockUserCreate).not.toHaveBeenCalled() + expect(mockUserInsertReturning).not.toHaveBeenCalled() }) it('defaults BOTH new axes to the narrow answer', async () => { @@ -131,9 +140,9 @@ describe('staff provisioning', () => { await POST(post({ name: 'Neue Person' })) - const [args] = mockUserCreate.mock.calls[0] - expect(args.data.scope).toBe('OWN_DOMAIN') - expect(args.data.isSystemAdmin).toBe(false) + const [values] = mockUserInsertReturning.mock.calls[0] + expect(values.scope).toBe('OWN_DOMAIN') + expect(values.isSystemAdmin).toBe(false) }) it('can describe Franziska: a Betreuerin who also sees everything', async () => { @@ -148,10 +157,10 @@ describe('staff provisioning', () => { await POST(post({ name: 'Franziska Heimhuber', role: 'BETREUUNG', scope: 'ALL_DOMAINS' })) - const [args] = mockUserCreate.mock.calls[0] - expect(args.data.role).toBe('BETREUUNG') - expect(args.data.scope).toBe('ALL_DOMAINS') - expect(args.data.isSystemAdmin).toBe(false) + const [values] = mockUserInsertReturning.mock.calls[0] + expect(values.role).toBe('BETREUUNG') + expect(values.scope).toBe('ALL_DOMAINS') + expect(values.isSystemAdmin).toBe(false) }) it('ignores a non-boolean isSystemAdmin rather than coercing it true', async () => { @@ -164,8 +173,8 @@ describe('staff provisioning', () => { await POST(post({ name: 'Neue Person', isSystemAdmin: 'yes' })) - const [args] = mockUserCreate.mock.calls[0] - expect(args.data.isSystemAdmin).toBe(false) + const [values] = mockUserInsertReturning.mock.calls[0] + expect(values.isSystemAdmin).toBe(false) }) it('rejects a role it does not recognise rather than falling back', async () => { @@ -179,7 +188,7 @@ describe('staff provisioning', () => { const response = await POST(post({ name: 'Neue Person', role: 'SUPERUSER' })) expect(response.status).toBe(400) - expect(mockUserCreate).not.toHaveBeenCalled() + expect(mockUserInsertReturning).not.toHaveBeenCalled() }) it('keeps the error generic enough not to confirm anything', async () => { diff --git a/src/app/api/cron/__tests__/notifications.test.ts b/src/app/api/cron/__tests__/notifications.test.ts index 07e6d0c2..af4cda2b 100644 --- a/src/app/api/cron/__tests__/notifications.test.ts +++ b/src/app/api/cron/__tests__/notifications.test.ts @@ -7,19 +7,22 @@ const mockIncidentFindMany = jest.fn() const mockPlacementFindMany = jest.fn() -// $queryRaw is used for pg_try_advisory_lock + pg_advisory_unlock. +// db.execute is used for pg_try_advisory_lock + pg_advisory_unlock. // Default mock: lock acquired so the route proceeds normally; tests can -// override per case. -const mockQueryRaw = jest.fn().mockResolvedValue([{ ok: true }]) +// override per case. db.execute resolves a pg result object ({ rows }). +const mockExecute = jest.fn().mockResolvedValue({ rows: [{ ok: true }] }) jest.mock('@/lib/db', () => ({ - prisma: { - incident: { - findMany: (...args: unknown[]) => mockIncidentFindMany(...args), - }, - placement: { - findMany: (...args: unknown[]) => mockPlacementFindMany(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + incident: { + findMany: (...args: unknown[]) => mockIncidentFindMany(...args), + }, + placement: { + findMany: (...args: unknown[]) => mockPlacementFindMany(...args), + }, }, - $queryRaw: (...args: unknown[]) => mockQueryRaw(...args), + execute: (...args: unknown[]) => mockExecute(...args), }, })) @@ -48,6 +51,7 @@ jest.mock('@/lib/logger', () => ({ // --- Import after mocks --- import { GET } from '../notifications/route' +import { PgDialect } from 'drizzle-orm/pg-core' // --- Helpers --- @@ -153,13 +157,13 @@ describe('overdue incident follow-ups', () => { await GET(createCronRequest(`Bearer ${CRON_SECRET}`)) - expect(mockIncidentFindMany).toHaveBeenCalledWith( - expect.objectContaining({ - where: expect.objectContaining({ - severity: { in: ['MEDIUM', 'HIGH', 'CRITICAL'] }, - }), - }), - ) + // The where arg is now a drizzle expression; render it to SQL to keep the + // assertion about the severity filter itself. + expect(mockIncidentFindMany).toHaveBeenCalledTimes(1) + const [args] = mockIncidentFindMany.mock.calls[0] as [{ where: never }] + const rendered = new PgDialect().sqlToQuery(args.where) + expect(rendered.sql).toContain('"severity" in (') + expect(rendered.params).toEqual(expect.arrayContaining(['MEDIUM', 'HIGH', 'CRITICAL'])) }) test('does not send when no overdue incidents', async () => { diff --git a/src/app/api/cron/__tests__/reset-demo.test.ts b/src/app/api/cron/__tests__/reset-demo.test.ts index 10a050b9..0c858c36 100644 --- a/src/app/api/cron/__tests__/reset-demo.test.ts +++ b/src/app/api/cron/__tests__/reset-demo.test.ts @@ -6,12 +6,14 @@ // --- Mocks --- -// $queryRaw serves pg_try_advisory_lock + pg_advisory_unlock. Default: +// db.execute serves pg_try_advisory_lock + pg_advisory_unlock. Default: // lock acquired so the route proceeds; tests override per case. -const mockQueryRaw = jest.fn() +// db.execute resolves a pg result object ({ rows }). +const mockExecute = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - $queryRaw: (...args: unknown[]) => mockQueryRaw(...args), + ...jest.requireActual('@/lib/db'), + db: { + execute: (...args: unknown[]) => mockExecute(...args), }, })) @@ -83,7 +85,7 @@ describe('POST /api/cron/reset-demo', () => { process.env.CRON_SECRET = CRON_SECRET process.env.DEMO_ACCESS_ENABLED = 'true' delete process.env.DEMO_RESET_SCOPE - mockQueryRaw.mockResolvedValue([{ ok: true }]) + mockExecute.mockResolvedValue({ rows: [{ ok: true }] }) mockResetDemoData.mockResolvedValue(FULL_RESET_SUMMARY) mockResetDemoWorld.mockResolvedValue(SCOPED_RESET_SUMMARY) }) @@ -128,7 +130,7 @@ describe('POST /api/cron/reset-demo', () => { describe('advisory lock', () => { it('skips when another reset already holds the lock', async () => { - mockQueryRaw.mockResolvedValueOnce([{ ok: false }]) + mockExecute.mockResolvedValueOnce({ rows: [{ ok: false }] }) const response = await POST(createCronRequest(`Bearer ${CRON_SECRET}`)) const body = await response.json() expect(body).toEqual({ skipped: true, reason: 'lock-held' }) @@ -138,15 +140,15 @@ describe('POST /api/cron/reset-demo', () => { it('releases the lock after a successful reset', async () => { await POST(createCronRequest(`Bearer ${CRON_SECRET}`)) - // First $queryRaw call acquires, second releases. - expect(mockQueryRaw).toHaveBeenCalledTimes(2) + // First db.execute call acquires, second releases. + expect(mockExecute).toHaveBeenCalledTimes(2) }) it('releases the lock even when the reset throws', async () => { mockResetDemoWorld.mockRejectedValueOnce(new Error('boom')) const response = await POST(createCronRequest(`Bearer ${CRON_SECRET}`)) expect(response.status).toBe(500) - expect(mockQueryRaw).toHaveBeenCalledTimes(2) + expect(mockExecute).toHaveBeenCalledTimes(2) }) }) diff --git a/src/app/api/export/__tests__/export-routes.test.ts b/src/app/api/export/__tests__/export-routes.test.ts index 67abae8d..b0c242fd 100644 --- a/src/app/api/export/__tests__/export-routes.test.ts +++ b/src/app/api/export/__tests__/export-routes.test.ts @@ -18,15 +18,20 @@ jest.mock('@/lib/auth', () => ({ const mockResidentFindMany = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findMany: (...args: unknown[]) => mockResidentFindMany(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { + findMany: (...args: unknown[]) => mockResidentFindMany(...args), + }, }, }, })) // --- Import after mocks --- import { GET } from '../../export/residents/route' +import { resident } from '@/lib/db' +import { desc } from 'drizzle-orm' describe('GET /api/export/residents', () => { beforeEach(() => { @@ -124,7 +129,7 @@ describe('GET /api/export/residents', () => { expect(lines[0]).toContain('Code') }) - it('calls prisma with correct orderBy', async () => { + it('queries with correct orderBy', async () => { mockGetCurrentUser.mockResolvedValue({ id: 'user-1', email: 'staff@aoz.ch', @@ -136,7 +141,7 @@ describe('GET /api/export/residents', () => { await GET() expect(mockResidentFindMany).toHaveBeenCalledWith({ - orderBy: { createdAt: 'desc' }, + orderBy: [desc(resident.createdAt)], }) }) }) diff --git a/src/app/api/import/__tests__/import-routes.test.ts b/src/app/api/import/__tests__/import-routes.test.ts index 71ff2d21..7977b949 100644 --- a/src/app/api/import/__tests__/import-routes.test.ts +++ b/src/app/api/import/__tests__/import-routes.test.ts @@ -16,27 +16,36 @@ jest.mock('@/lib/auth', () => ({ }, })) -// Route now uses createMany + a transaction; pre-checks existing codes via -// findMany, and writes AuditLog entries via auditLog.createMany. +// Route runs one transaction; pre-checks existing codes via tx.query.resident +// .findMany, bulk-inserts residents via tx.insert(resident).values(...) +// .onConflictDoNothing(), re-fetches the inserted IDs, and writes AuditLog rows +// via tx.insert(auditLog).values(...). The insert mocks receive the values +// payload (the array of rows) directly. const mockResidentFindMany = jest.fn().mockResolvedValue([]) -const mockResidentCreateMany = jest.fn().mockResolvedValue({ count: 0 }) -const mockAuditLogCreateMany = jest.fn().mockResolvedValue({ count: 0 }) -const mockTx = { - resident: { - findMany: mockResidentFindMany, - createMany: mockResidentCreateMany, - }, - auditLog: { - createMany: mockAuditLogCreateMany, - }, -} -const mockTransaction = jest.fn(async (cb: (tx: typeof mockTx) => Promise) => cb(mockTx)) -jest.mock('@/lib/db', () => ({ - prisma: { - $transaction: (...args: unknown[]) => - mockTransaction(...(args as [(tx: typeof mockTx) => Promise])), - }, -})) +const mockResidentCreateMany = jest.fn().mockResolvedValue(undefined) +const mockAuditLogCreateMany = jest.fn().mockResolvedValue(undefined) +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + const tx = { + query: { + resident: { findMany: (...args: unknown[]) => mockResidentFindMany(...args) }, + }, + insert: (table: unknown) => ({ + values: (v: unknown) => + table === actual.resident + ? // Resident insert is awaited after .onConflictDoNothing() + { onConflictDoNothing: (): Promise => mockResidentCreateMany(v) } + : // AuditLog insert is awaited on the builder itself (no returning) + mockAuditLogCreateMany(v), + }), + } + return { + ...actual, + db: { + transaction: (fn: (tx: unknown) => unknown) => fn(tx), + }, + } +}) const mockLogAudit = jest.fn() jest.mock('@/lib/audit', () => ({ @@ -144,8 +153,8 @@ describe('POST /api/import/residents', () => { mockResidentFindMany .mockResolvedValueOnce([]) // pre-check: no duplicates .mockResolvedValueOnce([{ id: 'new-res-1', code: 'RES-TEST' }]) // post-insert ID lookup - mockResidentCreateMany.mockResolvedValue({ count: 1 }) - mockAuditLogCreateMany.mockResolvedValue({ count: 1 }) + mockResidentCreateMany.mockResolvedValue(undefined) + mockAuditLogCreateMany.mockResolvedValue(undefined) mockLogAudit.mockResolvedValue(undefined) const csv = `${CSV_HEADER}\n${VALID_CSV_ROW}` @@ -162,34 +171,29 @@ describe('POST /api/import/residents', () => { expect(body.errors.length).toBe(0) // Verify bulk insert was called with the validated row payload - expect(mockResidentCreateMany).toHaveBeenCalledWith({ - data: [ - expect.objectContaining({ - code: 'RES-TEST', - ageRange: 'ADULT', - gender: 'MALE', - status: 'ACTIVE', - privacyNeed: 3, - guestTolerance: 3, - languages: ['de', 'en'], - }), - ], - skipDuplicates: true, - }) + expect(mockResidentCreateMany).toHaveBeenCalledWith([ + expect.objectContaining({ + code: 'RES-TEST', + ageRange: 'ADULT', + gender: 'MALE', + status: 'ACTIVE', + privacyNeed: 3, + guestTolerance: 3, + languages: ['de', 'en'], + }), + ]) // The route now also writes AuditLog rows inside the transaction plus a // single summary logAudit() afterwards. - expect(mockAuditLogCreateMany).toHaveBeenCalledWith({ - data: [ - expect.objectContaining({ - action: 'CREATE', - entity: 'RESIDENT', - entityId: 'new-res-1', - userId: 'user-1', - changes: { code: 'RES-TEST', source: 'CSV_IMPORT' }, - }), - ], - }) + expect(mockAuditLogCreateMany).toHaveBeenCalledWith([ + expect.objectContaining({ + action: 'CREATE', + entity: 'RESIDENT', + entityId: 'new-res-1', + userId: 'user-1', + changes: { code: 'RES-TEST', source: 'CSV_IMPORT' }, + }), + ]) expect(mockLogAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'CREATE', @@ -225,7 +229,7 @@ describe('POST /api/import/residents', () => { mockResidentFindMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([{ id: 'new-res-1', code: 'RES-TEST' }]) - mockResidentCreateMany.mockResolvedValue({ count: 1 }) + mockResidentCreateMany.mockResolvedValue(undefined) mockLogAudit.mockResolvedValue(undefined) // First row valid, second row invalid (missing code) @@ -265,7 +269,7 @@ describe('POST /api/import/residents', () => { mockResidentFindMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([{ id: 'new-res-1', code: 'RES-COERCE' }]) - mockResidentCreateMany.mockResolvedValue({ count: 1 }) + mockResidentCreateMany.mockResolvedValue(undefined) mockLogAudit.mockResolvedValue(undefined) // noiseTolerance and cleanlinessPractice come as strings from CSV @@ -275,14 +279,11 @@ describe('POST /api/import/residents', () => { await POST(request) - expect(mockResidentCreateMany).toHaveBeenCalledWith({ - data: [ - expect.objectContaining({ - noiseTolerance: 3, - cleanlinessPractice: 4, - }), - ], - skipDuplicates: true, - }) + expect(mockResidentCreateMany).toHaveBeenCalledWith([ + expect.objectContaining({ + noiseTolerance: 3, + cleanlinessPractice: 4, + }), + ]) }) }) diff --git a/src/app/api/portal/__tests__/chores.test.ts b/src/app/api/portal/__tests__/chores.test.ts index cd60b12b..2e2d7ff4 100644 --- a/src/app/api/portal/__tests__/chores.test.ts +++ b/src/app/api/portal/__tests__/chores.test.ts @@ -25,48 +25,65 @@ const mockFindMany = jest.fn() const mockFindFirst = jest.fn() const mockCreate = jest.fn() const mockUpdate = jest.fn() -const mockGroupBy = jest.fn() -const mockCompletionFindMany = jest.fn() +// Balance loader uses db.select().from(taskCompletion/placement) with joins. +const mockCompletionSelect = jest.fn().mockResolvedValue([]) +const mockMemberSelect = jest.fn().mockResolvedValue([]) const mockIncidentCreate = jest.fn() const mockFlagCreate = jest.fn() const mockRequestCreate = jest.fn() const mockTransaction = jest.fn() -const mockCompletionCreate = jest.fn() -const mockFlagUpdateMany = jest.fn() -const mockRequestUpdateMany = jest.fn() -const mockTaskUpdate = jest.fn() - -jest.mock('@/lib/db', () => ({ - prisma: { - householdTask: { - findMany: (...args: unknown[]) => mockFindMany(...args), - findFirst: (...args: unknown[]) => mockFindFirst(...args), - create: (...args: unknown[]) => mockCreate(...args), - update: (...args: unknown[]) => mockUpdate(...args), - }, - taskCompletion: { - groupBy: (...args: unknown[]) => mockGroupBy(...args), - create: (...args: unknown[]) => mockCompletionCreate(...args), - findMany: (...args: unknown[]) => mockCompletionFindMany(...args), - }, - taskAttentionFlag: { - create: (...args: unknown[]) => mockFlagCreate(...args), - updateMany: (...args: unknown[]) => mockFlagUpdateMany(...args), - }, - taskRequest: { - create: (...args: unknown[]) => mockRequestCreate(...args), - updateMany: (...args: unknown[]) => mockRequestUpdateMany(...args), - }, - placement: { - findMany: (...args: unknown[]) => mockFindMany(...args), - findFirst: (...args: unknown[]) => mockFindFirst(...args), - }, - incident: { - create: (...args: unknown[]) => mockIncidentCreate(...args), + +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + // db.select().from(x).innerJoin(...).where(...)[.orderBy(...)] — a chainable + // builder that is awaited at the end of the chain (thenable). + const selectChain = (table: unknown) => { + const resolve = (): Promise => + table === actual.taskCompletion ? mockCompletionSelect() : mockMemberSelect() + const chain = { + innerJoin: () => chain, + where: () => chain, + orderBy: () => chain, + then: (onFulfilled?: (v: unknown) => unknown, onRejected?: (e: unknown) => unknown) => + resolve().then(onFulfilled, onRejected), + } + return chain + } + return { + ...actual, + db: { + query: { + householdTask: { + findMany: (...args: unknown[]) => mockFindMany(...args), + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, + placement: { + findMany: (...args: unknown[]) => mockFindMany(...args), + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, + }, + insert: (table: unknown) => ({ + values: (v: unknown) => ({ + returning: (): Promise => + table === actual.householdTask + ? mockCreate(v) + : table === actual.incident + ? mockIncidentCreate(v) + : table === actual.taskAttentionFlag + ? mockFlagCreate(v) + : mockRequestCreate(v), + }), + }), + update: (_table: unknown) => ({ + set: (v: unknown) => ({ + where: (w: unknown): Promise => mockUpdate({ set: v, where: w }), + }), + }), + select: () => ({ from: selectChain }), + transaction: (...args: unknown[]) => mockTransaction(...args), }, - $transaction: (...args: unknown[]) => mockTransaction(...args), - }, -})) + } +}) const mockLogAudit = jest.fn().mockResolvedValue(undefined) jest.mock('@/lib/audit', () => ({ @@ -113,6 +130,8 @@ import { POST as completeChore } from '../chores/[id]/complete/route' import { POST as complainChore } from '../chores/[id]/complaint/route' import { POST as attentionChore } from '../chores/[id]/attention/route' import { POST as requestChore } from '../chores/[id]/request/route' +import { householdTask, taskAttentionFlag, taskRequest, placement } from '@/lib/db' +import { eq, and, ne, inArray } from 'drizzle-orm' // --- Helpers --- @@ -144,6 +163,35 @@ function makeParams(id: string) { return { params: Promise.resolve({ id }) } } +/** + * A drizzle-shaped transaction stub for the complete route: the tx inserts one + * TaskCompletion (with .returning()) and issues three .update().set().where() + * calls, dispatched per table so each can be asserted independently. Update + * mocks receive `{ set, where }`. + */ +function makeTx(completionResult: { id: string }) { + const completionCreate = jest.fn().mockResolvedValue([completionResult]) + const taskUpdate = jest.fn().mockResolvedValue(undefined) + const flagUpdateMany = jest.fn().mockResolvedValue(undefined) + const requestUpdateMany = jest.fn().mockResolvedValue(undefined) + const tx = { + insert: () => ({ + values: (v: unknown) => ({ returning: (): Promise => completionCreate(v) }), + }), + update: (table: unknown) => ({ + set: (v: unknown) => ({ + where: (w: unknown): Promise => + table === householdTask + ? taskUpdate({ set: v, where: w }) + : table === taskAttentionFlag + ? flagUpdateMany({ set: v, where: w }) + : requestUpdateMany({ set: v, where: w }), + }), + }), + } + return { tx, completionCreate, taskUpdate, flagUpdateMany, requestUpdateMany } +} + const SAMPLE_TASK = { id: 'task-1', housingUnitId: 'hu-1', @@ -182,48 +230,47 @@ describe('GET /api/portal/chores', () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) const tasks = [{ id: 'task-1', title: 'Küche putzen' }] - const roommates = [ - { resident: { id: 'res-1', code: 'RES-001', displayName: null } }, - { resident: { id: 'res-2', code: 'RES-002', displayName: null } }, + const members = [ + { id: 'res-1', code: 'RES-001', displayName: null }, + { id: 'res-2', code: 'RES-002', displayName: null }, ] // Four quick bin runs vs one long shower scrub: counting rows would call // res-1 the bigger contributor 4:1. The balance must say the opposite. - mockCompletionFindMany.mockResolvedValue([ + mockCompletionSelect.mockResolvedValue([ { completedById: 'res-1', completedAt: new Date(), durationMinutes: 5, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, { completedById: 'res-1', completedAt: new Date(), durationMinutes: 5, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, { completedById: 'res-1', completedAt: new Date(), durationMinutes: 5, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, { completedById: 'res-1', completedAt: new Date(), durationMinutes: 5, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, { completedById: 'res-2', completedAt: new Date(), durationMinutes: 40, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, ]) - mockFindMany - .mockResolvedValueOnce(tasks) // householdTask.findMany - .mockResolvedValueOnce(roommates) // placement.findMany + mockMemberSelect.mockResolvedValue(members) + mockFindMany.mockResolvedValueOnce(tasks) // householdTask.findMany const res = await listChores() const body = await res.json() @@ -254,10 +301,9 @@ describe('GET /api/portal/chores', () => { test('balance shows a zero line for residents who did nothing yet', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) - mockCompletionFindMany.mockResolvedValue([]) - mockFindMany - .mockResolvedValueOnce([]) // no tasks - .mockResolvedValueOnce([{ resident: { id: 'res-3', code: 'RES-003', displayName: null } }]) + mockCompletionSelect.mockResolvedValue([]) + mockMemberSelect.mockResolvedValue([{ id: 'res-3', code: 'RES-003', displayName: null }]) + mockFindMany.mockResolvedValueOnce([]) // no tasks const res = await listChores() const body = await res.json() @@ -284,17 +330,16 @@ describe('GET /api/portal/chores', () => { lastMonth.setUTCHours(12, 0, 0, 0) lastMonth.setUTCDate(lastMonth.getUTCDate() - 5) - mockCompletionFindMany.mockResolvedValue([ + mockCompletionSelect.mockResolvedValue([ { completedById: 'res-1', completedAt: lastMonth, durationMinutes: 90, - task: { estimatedMinutes: null }, + taskEstimatedMinutes: null, }, ]) - mockFindMany - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([{ resident: { id: 'res-1', code: 'RES-001', displayName: null } }]) + mockMemberSelect.mockResolvedValue([{ id: 'res-1', code: 'RES-001', displayName: null }]) + mockFindMany.mockResolvedValueOnce([]) const res = await listChores() const body = await res.json() @@ -316,15 +361,16 @@ describe('GET /api/portal/chores', () => { test('scopes tasks to authenticated resident housing unit', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) - mockFindMany.mockResolvedValueOnce([]).mockResolvedValueOnce([]) - mockGroupBy.mockResolvedValue([]) + mockFindMany.mockResolvedValueOnce([]) + mockCompletionSelect.mockResolvedValue([]) + mockMemberSelect.mockResolvedValue([]) await listChores() // First findMany call is for householdTask, check the where clause expect(mockFindMany).toHaveBeenCalledWith( expect.objectContaining({ - where: { housingUnitId: 'hu-1' }, + where: eq(householdTask.housingUnitId, 'hu-1'), }), ) }) @@ -400,7 +446,7 @@ describe('POST /api/portal/chores', () => { estimatedMinutes: 30, checklist: ['Boden gewischt', 'Abfalleimer geleert'], }) - mockCreate.mockResolvedValue({ id: 'task-new' }) + mockCreate.mockResolvedValue([{ id: 'task-new' }]) const req = createFormDataRequest('http://localhost:3001/api/portal/chores', { title: 'Küche putzen', @@ -413,19 +459,17 @@ describe('POST /api/portal/chores', () => { expect(body.data).toEqual({ id: 'task-new' }) expect(mockCreate).toHaveBeenCalledWith({ - data: { - housingUnitId: 'hu-1', - createdByResidentId: 'res-1', - title: 'Küche putzen', - description: 'Boden wischen', - instructions: 'Mit warmem Wasser', - taskType: 'RECURRING', - category: 'CLEANING', - priority: 'HIGH', - scheduleHuman: 'Jeden Montag', - estimatedMinutes: 30, - checklist: ['Boden gewischt', 'Abfalleimer geleert'], - }, + housingUnitId: 'hu-1', + createdByResidentId: 'res-1', + title: 'Küche putzen', + description: 'Boden wischen', + instructions: 'Mit warmem Wasser', + taskType: 'RECURRING', + category: 'CLEANING', + priority: 'HIGH', + scheduleHuman: 'Jeden Montag', + estimatedMinutes: 30, + checklist: ['Boden gewischt', 'Abfalleimer geleert'], }) }) @@ -437,21 +481,21 @@ describe('POST /api/portal/chores', () => { category: 'TRASH', priority: 'NORMAL', }) - mockCreate.mockResolvedValue({ id: 'task-2' }) + mockCreate.mockResolvedValue([{ id: 'task-2' }]) const req = createFormDataRequest('http://localhost:3001/api/portal/chores', { title: 'Müll rausbringen', }) await createChore(req) - expect(mockCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ description: null, instructions: null, scheduleHuman: null, estimatedMinutes: null, }), - }) + ) }) test('calls logAudit after successful creation', async () => { @@ -462,7 +506,7 @@ describe('POST /api/portal/chores', () => { category: 'CLEANING', priority: 'NORMAL', }) - mockCreate.mockResolvedValue({ id: 'task-audit' }) + mockCreate.mockResolvedValue([{ id: 'task-audit' }]) const req = createFormDataRequest('http://localhost:3001/api/portal/chores', { title: 'Putzen', @@ -547,7 +591,7 @@ describe('GET /api/portal/chores/[id]', () => { expect(mockFindFirst).toHaveBeenCalledWith( expect.objectContaining({ - where: { id: 'task-other', housingUnitId: 'hu-1' }, + where: and(eq(householdTask.id, 'task-other'), eq(householdTask.housingUnitId, 'hu-1')), }), ) }) @@ -589,9 +633,11 @@ describe('GET /api/portal/chores/[id]', () => { // Verify placement.findMany excludes the current resident expect(mockFindMany).toHaveBeenCalledWith( expect.objectContaining({ - where: expect.objectContaining({ - residentId: { not: 'res-1' }, - }), + where: and( + eq(placement.housingUnitId, 'hu-1'), + eq(placement.status, 'ACTIVE'), + ne(placement.residentId, 'res-1'), + ), }), ) }) @@ -662,23 +708,9 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, taskType: 'RECURRING' }) const completionResult = { id: 'comp-1' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { - create: jest.fn().mockResolvedValue(completionResult), - }, - householdTask: { - update: jest.fn().mockResolvedValue({}), - }, - taskAttentionFlag: { - updateMany: jest.fn().mockResolvedValue({ count: 0 }), - }, - taskRequest: { - updateMany: jest.fn().mockResolvedValue({ count: 0 }), - }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete') const res = await completeChore(req, makeParams('task-1')) @@ -689,18 +721,13 @@ describe('POST /api/portal/chores/[id]/complete', () => { // Verify the transaction callback updates the task const txFn = mockTransaction.mock.calls[0][0] - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await txFn(mockTx) + const replay = makeTx(completionResult) + await txFn(replay.tx) // For RECURRING tasks, should NOT set isCompleted/completedAt - expect(mockTx.householdTask.update).toHaveBeenCalledWith({ - where: { id: 'task-1' }, - data: { currentStatus: 'IDLE' }, + expect(replay.taskUpdate).toHaveBeenCalledWith({ + set: { currentStatus: 'IDLE' }, + where: eq(householdTask.id, 'task-1'), }) }) @@ -708,12 +735,7 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK }) mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => - fn({ - taskCompletion: { create: jest.fn().mockResolvedValue({ id: 'comp-1' }) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - }), + fn(makeTx({ id: 'comp-1' }).tx), ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete', { @@ -723,18 +745,11 @@ describe('POST /api/portal/chores/[id]/complete', () => { }) await completeChore(req, makeParams('task-1')) - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue({ id: 'comp-1' }) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await mockTransaction.mock.calls[0][0](mockTx) + const replay = makeTx({ id: 'comp-1' }) + await mockTransaction.mock.calls[0][0](replay.tx) - expect(mockTx.taskCompletion.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ completedItems: ['Boden gewischt'] }), - }), + expect(replay.completionCreate).toHaveBeenCalledWith( + expect.objectContaining({ completedItems: ['Boden gewischt'] }), ) }) @@ -742,12 +757,7 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK }) mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => - fn({ - taskCompletion: { create: jest.fn().mockResolvedValue({ id: 'comp-1' }) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - }), + fn(makeTx({ id: 'comp-1' }).tx), ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete', { @@ -756,18 +766,11 @@ describe('POST /api/portal/chores/[id]/complete', () => { const res = await completeChore(req, makeParams('task-1')) expect(res.status).toBe(200) - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue({ id: 'comp-1' }) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await mockTransaction.mock.calls[0][0](mockTx) + const replay = makeTx({ id: 'comp-1' }) + await mockTransaction.mock.calls[0][0](replay.tx) - expect(mockTx.taskCompletion.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ completedItems: ['Abfalleimer geleert'] }), - }), + expect(replay.completionCreate).toHaveBeenCalledWith( + expect.objectContaining({ completedItems: ['Abfalleimer geleert'] }), ) }) @@ -776,15 +779,9 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, taskType: 'ONE_TIME' }) const completionResult = { id: 'comp-2' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete') const res = await completeChore(req, makeParams('task-1')) @@ -793,21 +790,16 @@ describe('POST /api/portal/chores/[id]/complete', () => { // Verify ONE_TIME sets isCompleted + completedAt const txFn = mockTransaction.mock.calls[0][0] - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await txFn(mockTx) + const replay = makeTx(completionResult) + await txFn(replay.tx) - expect(mockTx.householdTask.update).toHaveBeenCalledWith({ - where: { id: 'task-1' }, - data: expect.objectContaining({ + expect(replay.taskUpdate).toHaveBeenCalledWith({ + set: expect.objectContaining({ currentStatus: 'IDLE', isCompleted: true, completedAt: expect.any(Date), }), + where: eq(householdTask.id, 'task-1'), }) }) @@ -816,36 +808,25 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue(SAMPLE_TASK) const completionResult = { id: 'comp-3' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 2 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete') await completeChore(req, makeParams('task-1')) // Verify flags are resolved const txFn = mockTransaction.mock.calls[0][0] - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 2 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await txFn(mockTx) + const replay = makeTx(completionResult) + await txFn(replay.tx) - expect(mockTx.taskAttentionFlag.updateMany).toHaveBeenCalledWith({ - where: { taskId: 'task-1', isResolved: false }, - data: { + expect(replay.flagUpdateMany).toHaveBeenCalledWith({ + set: { isResolved: true, resolvedAt: expect.any(Date), resolvedByCompletionId: 'comp-3', }, + where: and(eq(taskAttentionFlag.taskId, 'task-1'), eq(taskAttentionFlag.isResolved, false)), }) }) @@ -854,37 +835,26 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue(SAMPLE_TASK) const completionResult = { id: 'comp-4' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete') await completeChore(req, makeParams('task-1')) const txFn = mockTransaction.mock.calls[0][0] - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 1 }) }, - } - await txFn(mockTx) + const replay = makeTx(completionResult) + await txFn(replay.tx) - expect(mockTx.taskRequest.updateMany).toHaveBeenCalledWith({ - where: { - taskId: 'task-1', - status: { in: ['PENDING', 'ACCEPTED'] }, - }, - data: { + expect(replay.requestUpdateMany).toHaveBeenCalledWith({ + set: { status: 'COMPLETED', completionId: 'comp-4', }, + where: and( + eq(taskRequest.taskId, 'task-1'), + inArray(taskRequest.status, ['PENDING', 'ACCEPTED']), + ), }) }) @@ -893,15 +863,9 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue(SAMPLE_TASK) const completionResult = { id: 'comp-5' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete', { notes: 'Alles erledigt', @@ -911,22 +875,17 @@ describe('POST /api/portal/chores/[id]/complete', () => { // Verify the notes/duration are passed through the transaction const txFn = mockTransaction.mock.calls[0][0] - const mockTx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - await txFn(mockTx) + const replay = makeTx(completionResult) + await txFn(replay.tx) - expect(mockTx.taskCompletion.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(replay.completionCreate).toHaveBeenCalledWith( + expect.objectContaining({ taskId: 'task-1', completedById: 'res-1', notes: 'Alles erledigt', durationMinutes: 15, }), - }) + ) }) test('works with empty body (quick-complete)', async () => { @@ -934,15 +893,9 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockFindFirst.mockResolvedValue(SAMPLE_TASK) const completionResult = { id: 'comp-6' } - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue(completionResult) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx(completionResult).tx), + ) // No body at all const req = new NextRequest('http://localhost:3001/api/portal/chores/task-1/complete', { @@ -959,15 +912,9 @@ describe('POST /api/portal/chores/[id]/complete', () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => { - const tx = { - taskCompletion: { create: jest.fn().mockResolvedValue({ id: 'comp-audit' }) }, - householdTask: { update: jest.fn().mockResolvedValue({}) }, - taskAttentionFlag: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - taskRequest: { updateMany: jest.fn().mockResolvedValue({ count: 0 }) }, - } - return fn(tx) - }) + mockTransaction.mockImplementation(async (fn: (tx: unknown) => Promise) => + fn(makeTx({ id: 'comp-audit' }).tx), + ) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complete') await completeChore(req, makeParams('task-1')) @@ -1003,7 +950,7 @@ describe('POST /api/portal/chores/[id]/complete', () => { await completeChore(req, makeParams('task-1')) expect(mockFindFirst).toHaveBeenCalledWith({ - where: { id: 'task-1', housingUnitId: 'hu-1' }, + where: and(eq(householdTask.id, 'task-1'), eq(householdTask.housingUnitId, 'hu-1')), }) }) }) @@ -1077,7 +1024,7 @@ describe('POST /api/portal/chores/[id]/complaint', () => { test('creates incident with CLEANING category mapped to CLEANLINESS_DISPUTE', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, category: 'CLEANING' }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-1' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-1' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Küche nicht geputzt', @@ -1089,8 +1036,8 @@ describe('POST /api/portal/chores/[id]/complaint', () => { expect(body.success).toBe(true) expect(body.data.incidentId).toBe('inc-1') - expect(mockIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ housingUnitId: 'hu-1', placementId: 'pl-1', reportedById: 'res-1', @@ -1100,55 +1047,55 @@ describe('POST /api/portal/chores/[id]/complaint', () => { description: '[Haushaltsaufgabe: Küche putzen]\n\nKüche nicht geputzt', date: expect.any(Date), }), - }) + ) }) test('maps COOKING category to SPACE_DISPUTE incident type', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, category: 'COOKING', title: 'Kochen' }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-2' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-2' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Kochplan nicht eingehalten', }) await complainChore(req, makeParams('task-1')) - expect(mockIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SPACE_DISPUTE', description: '[Haushaltsaufgabe: Kochen]\n\nKochplan nicht eingehalten', }), - }) + ) }) test('maps TRASH category to CLEANLINESS_DISPUTE incident type', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, category: 'TRASH', title: 'Müll' }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-3' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-3' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Müll nicht rausgebracht', }) await complainChore(req, makeParams('task-1')) - expect(mockIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ type: 'CLEANLINESS_DISPUTE' }), - }) + expect(mockIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ type: 'CLEANLINESS_DISPUTE' }), + ) }) test('maps MAINTENANCE category to GENERAL_MAINTENANCE incident type', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue({ ...SAMPLE_TASK, category: 'MAINTENANCE', title: 'Reparatur' }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-4' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-4' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Nicht repariert', }) await complainChore(req, makeParams('task-1')) - expect(mockIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ type: 'GENERAL_MAINTENANCE' }), - }) + expect(mockIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ type: 'GENERAL_MAINTENANCE' }), + ) }) test('falls back to PERSONAL_CONFLICT for unknown categories', async () => { @@ -1158,22 +1105,22 @@ describe('POST /api/portal/chores/[id]/complaint', () => { category: 'UNKNOWN_CATEGORY', title: 'Custom', }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-5' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-5' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Problem', }) await complainChore(req, makeParams('task-1')) - expect(mockIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ type: 'PERSONAL_CONFLICT' }), - }) + expect(mockIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ type: 'PERSONAL_CONFLICT' }), + ) }) test('calls logAudit with correct complaint data', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockIncidentCreate.mockResolvedValue({ id: 'inc-audit' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-audit' }]) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/complaint', { description: 'Beschwerde', @@ -1261,7 +1208,7 @@ describe('POST /api/portal/chores/[id]/attention', () => { test('creates attention flag and updates task status', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockFlagCreate.mockResolvedValue({ id: 'flag-1' }) + mockFlagCreate.mockResolvedValue([{ id: 'flag-1' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/attention', { @@ -1275,23 +1222,21 @@ describe('POST /api/portal/chores/[id]/attention', () => { expect(body.data).toEqual({ id: 'flag-1' }) expect(mockFlagCreate).toHaveBeenCalledWith({ - data: { - taskId: 'task-1', - flaggedById: 'res-1', - message: 'Dringend!', - }, + taskId: 'task-1', + flaggedById: 'res-1', + message: 'Dringend!', }) expect(mockUpdate).toHaveBeenCalledWith({ - where: { id: 'task-1' }, - data: { currentStatus: 'NEEDS_ATTENTION' }, + set: { currentStatus: 'NEEDS_ATTENTION' }, + where: eq(householdTask.id, 'task-1'), }) }) test('works without body (empty flag)', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockFlagCreate.mockResolvedValue({ id: 'flag-2' }) + mockFlagCreate.mockResolvedValue([{ id: 'flag-2' }]) mockUpdate.mockResolvedValue({}) const req = new NextRequest('http://localhost:3001/api/portal/chores/task-1/attention', { @@ -1304,18 +1249,16 @@ describe('POST /api/portal/chores/[id]/attention', () => { expect(body.success).toBe(true) expect(mockFlagCreate).toHaveBeenCalledWith({ - data: { - taskId: 'task-1', - flaggedById: 'res-1', - message: null, - }, + taskId: 'task-1', + flaggedById: 'res-1', + message: null, }) }) test('sets message to null when not provided in JSON body', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockFlagCreate.mockResolvedValue({ id: 'flag-3' }) + mockFlagCreate.mockResolvedValue([{ id: 'flag-3' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/attention', {}) @@ -1323,11 +1266,11 @@ describe('POST /api/portal/chores/[id]/attention', () => { expect(res.status).toBe(200) - expect(mockFlagCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockFlagCreate).toHaveBeenCalledWith( + expect.objectContaining({ message: null, }), - }) + ) }) test('returns 500 when flag creation fails', async () => { @@ -1352,7 +1295,7 @@ describe('POST /api/portal/chores/[id]/attention', () => { await attentionChore(req, makeParams('task-1')) expect(mockFindFirst).toHaveBeenCalledWith({ - where: { id: 'task-1', housingUnitId: 'hu-1' }, + where: and(eq(householdTask.id, 'task-1'), eq(householdTask.housingUnitId, 'hu-1')), }) }) }) @@ -1424,7 +1367,7 @@ describe('POST /api/portal/chores/[id]/request', () => { test('creates targeted request when requestedResidentId is provided', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockRequestCreate.mockResolvedValue({ id: 'req-1' }) + mockRequestCreate.mockResolvedValue([{ id: 'req-1' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest( @@ -1441,20 +1384,18 @@ describe('POST /api/portal/chores/[id]/request', () => { expect(body.data).toEqual({ id: 'req-1' }) expect(mockRequestCreate).toHaveBeenCalledWith({ - data: { - taskId: 'task-1', - requestedById: 'res-1', - requestedResidentId: 'cjld2cjxh0000qzrmn831i7rn', - isBroadcast: false, - message: 'Bitte erledigen', - }, + taskId: 'task-1', + requestedById: 'res-1', + requestedResidentId: 'cjld2cjxh0000qzrmn831i7rn', + isBroadcast: false, + message: 'Bitte erledigen', }) }) test('creates broadcast request when no requestedResidentId', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockRequestCreate.mockResolvedValue({ id: 'req-2' }) + mockRequestCreate.mockResolvedValue([{ id: 'req-2' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/request', { @@ -1467,45 +1408,43 @@ describe('POST /api/portal/chores/[id]/request', () => { expect(body.success).toBe(true) expect(mockRequestCreate).toHaveBeenCalledWith({ - data: { - taskId: 'task-1', - requestedById: 'res-1', - requestedResidentId: null, - isBroadcast: true, - message: 'Wer kann das machen?', - }, + taskId: 'task-1', + requestedById: 'res-1', + requestedResidentId: null, + isBroadcast: true, + message: 'Wer kann das machen?', }) }) test('updates task status to REQUESTED', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockRequestCreate.mockResolvedValue({ id: 'req-3' }) + mockRequestCreate.mockResolvedValue([{ id: 'req-3' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/request', {}) await requestChore(req, makeParams('task-1')) expect(mockUpdate).toHaveBeenCalledWith({ - where: { id: 'task-1' }, - data: { currentStatus: 'REQUESTED' }, + set: { currentStatus: 'REQUESTED' }, + where: eq(householdTask.id, 'task-1'), }) }) test('sets message to null when not provided', async () => { mockGetPortalAuth.mockResolvedValue(AUTH_RESULT) mockFindFirst.mockResolvedValue(SAMPLE_TASK) - mockRequestCreate.mockResolvedValue({ id: 'req-4' }) + mockRequestCreate.mockResolvedValue([{ id: 'req-4' }]) mockUpdate.mockResolvedValue({}) const req = createJsonRequest('http://localhost:3001/api/portal/chores/task-1/request', {}) await requestChore(req, makeParams('task-1')) - expect(mockRequestCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockRequestCreate).toHaveBeenCalledWith( + expect.objectContaining({ message: null, }), - }) + ) }) test('returns 500 when request creation fails', async () => { @@ -1530,7 +1469,7 @@ describe('POST /api/portal/chores/[id]/request', () => { await requestChore(req, makeParams('task-1')) expect(mockFindFirst).toHaveBeenCalledWith({ - where: { id: 'task-1', housingUnitId: 'hu-1' }, + where: and(eq(householdTask.id, 'task-1'), eq(householdTask.housingUnitId, 'hu-1')), }) }) }) diff --git a/src/app/api/portal/__tests__/expenses.test.ts b/src/app/api/portal/__tests__/expenses.test.ts index f8620215..1df7e941 100644 --- a/src/app/api/portal/__tests__/expenses.test.ts +++ b/src/app/api/portal/__tests__/expenses.test.ts @@ -19,22 +19,38 @@ jest.mock('@/lib/portal-auth', () => ({ getActiveUnitMembers: (...args: unknown[]) => mockGetActiveUnitMembers(...args), })) +// The expense route inserts Expense + ExpenseShare rows inside one +// transaction; shares get their own mock so the split stays assertable. const mockExpenseCreate = jest.fn() -const mockExpenseFindUnique = jest.fn() +const mockShareCreate = jest.fn() +const mockExpenseFindFirst = jest.fn() const mockExpenseDelete = jest.fn() const mockSettlementCreate = jest.fn() -jest.mock('@/lib/db', () => ({ - prisma: { - expense: { - create: (...args: unknown[]) => mockExpenseCreate(...args), - findUnique: (...args: unknown[]) => mockExpenseFindUnique(...args), - delete: (...args: unknown[]) => mockExpenseDelete(...args), - }, - settlement: { - create: (...args: unknown[]) => mockSettlementCreate(...args), +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + return { + ...actual, + db: { + query: { + expense: { findFirst: (...args: unknown[]) => mockExpenseFindFirst(...args) }, + }, + // Top-level insert is only used for settlements. + insert: () => ({ + values: (v: unknown) => ({ returning: (): Promise => mockSettlementCreate(v) }), + }), + delete: () => ({ where: (w: unknown): Promise => mockExpenseDelete(w) }), + transaction: (fn: (tx: unknown) => unknown) => + fn({ + insert: (table: unknown) => ({ + values: (v: unknown) => ({ + returning: (): Promise => + table === actual.expense ? mockExpenseCreate(v) : mockShareCreate(v), + }), + }), + }), }, - }, -})) + } +}) const mockLogAudit = jest.fn().mockResolvedValue(undefined) jest.mock('@/lib/audit', () => ({ @@ -56,6 +72,8 @@ jest.mock('@/lib/logger', () => ({ import { POST as createExpense } from '../expenses/route' import { DELETE as deleteExpense } from '../expenses/[id]/route' import { POST as createSettlement } from '../settlements/route' +import { expense } from '@/lib/db' +import { eq } from 'drizzle-orm' // --- Helpers --- @@ -85,11 +103,12 @@ beforeEach(() => { jest.clearAllMocks() mockGetPortalAuth.mockResolvedValue(AUTH) mockGetActiveUnitMembers.mockResolvedValue(MEMBERS) - mockExpenseCreate.mockImplementation((args: { data: unknown }) => - Promise.resolve({ id: 'expense-1', ...(args.data as object), shares: [] }), + mockExpenseCreate.mockImplementation((values: unknown) => + Promise.resolve([{ id: 'expense-1', ...(values as object) }]), ) - mockSettlementCreate.mockImplementation((args: { data: unknown }) => - Promise.resolve({ id: 'settlement-1', ...(args.data as object) }), + mockShareCreate.mockImplementation((rows: unknown) => Promise.resolve(rows as unknown[])) + mockSettlementCreate.mockImplementation((values: unknown) => + Promise.resolve([{ id: 'settlement-1', ...(values as object) }]), ) }) @@ -104,12 +123,17 @@ describe('POST /api/portal/expenses', () => { const response = await createExpense(jsonRequest('/api/portal/expenses', 'POST', VALID_EXPENSE)) expect(response.status).toBe(200) - const created = mockExpenseCreate.mock.calls[0][0].data + const created = mockExpenseCreate.mock.calls[0][0] expect(created.paidById).toBe('georgy') expect(created.createdById).toBe('georgy') - const shares = created.shares.create as { residentId: string; amountRappen: number }[] + const shares = mockShareCreate.mock.calls[0][0] as { + residentId: string + amountRappen: number + expenseId: string + }[] expect(shares).toHaveLength(4) expect(shares.reduce((a, s) => a + s.amountRappen, 0)).toBe(4000) + expect(shares.every((s) => s.expenseId === 'expense-1')).toBe(true) }) it('allows recording on behalf of another roommate with chosen participants', async () => { @@ -121,10 +145,10 @@ describe('POST /api/portal/expenses', () => { }), ) expect(response.status).toBe(200) - const created = mockExpenseCreate.mock.calls[0][0].data + const created = mockExpenseCreate.mock.calls[0][0] expect(created.paidById).toBe('ihor') expect(created.createdById).toBe('georgy') - expect(created.shares.create).toHaveLength(2) + expect(mockShareCreate.mock.calls[0][0]).toHaveLength(2) }) it('rejects a payer who does not live in the unit', async () => { @@ -180,27 +204,27 @@ describe('DELETE /api/portal/expenses/[id]', () => { } it('lets the creator delete', async () => { - mockExpenseFindUnique.mockResolvedValue(EXPENSE) + mockExpenseFindFirst.mockResolvedValue(EXPENSE) const response = await del() expect(response.status).toBe(200) - expect(mockExpenseDelete).toHaveBeenCalledWith({ where: { id: 'expense-1' } }) + expect(mockExpenseDelete).toHaveBeenCalledWith(eq(expense.id, 'expense-1')) }) it('lets the payer delete', async () => { - mockExpenseFindUnique.mockResolvedValue({ ...EXPENSE, paidById: 'georgy', createdById: 'ihor' }) + mockExpenseFindFirst.mockResolvedValue({ ...EXPENSE, paidById: 'georgy', createdById: 'ihor' }) const response = await del() expect(response.status).toBe(200) }) it('refuses an uninvolved roommate with 403', async () => { - mockExpenseFindUnique.mockResolvedValue({ ...EXPENSE, paidById: 'misha', createdById: 'alex' }) + mockExpenseFindFirst.mockResolvedValue({ ...EXPENSE, paidById: 'misha', createdById: 'alex' }) const response = await del() expect(response.status).toBe(403) expect(mockExpenseDelete).not.toHaveBeenCalled() }) it("hides another unit's expense as 404", async () => { - mockExpenseFindUnique.mockResolvedValue({ ...EXPENSE, housingUnitId: 'unit-2' }) + mockExpenseFindFirst.mockResolvedValue({ ...EXPENSE, housingUnitId: 'unit-2' }) const response = await del() expect(response.status).toBe(404) expect(mockExpenseDelete).not.toHaveBeenCalled() @@ -213,7 +237,7 @@ describe('POST /api/portal/settlements', () => { jsonRequest('/api/portal/settlements', 'POST', { toResidentId: 'ihor', amountRappen: 1500 }), ) expect(response.status).toBe(200) - const created = mockSettlementCreate.mock.calls[0][0].data + const created = mockSettlementCreate.mock.calls[0][0] expect(created).toMatchObject({ fromId: 'georgy', toId: 'ihor', amountRappen: 1500 }) }) diff --git a/src/app/api/portal/__tests__/preferences.test.ts b/src/app/api/portal/__tests__/preferences.test.ts index de5408f2..204d9c07 100644 --- a/src/app/api/portal/__tests__/preferences.test.ts +++ b/src/app/api/portal/__tests__/preferences.test.ts @@ -16,14 +16,22 @@ jest.mock('next/headers', () => ({ }), })) -const mockFindUnique = jest.fn() +const mockFindFirst = jest.fn() const mockUpdate = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findUnique: (...args: unknown[]) => mockFindUnique(...args), - update: (...args: unknown[]) => mockUpdate(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, }, + // db.update(resident).set(v).where(w) — the mock receives { set, where }. + update: () => ({ + set: (v: unknown) => ({ + where: (w: unknown): Promise => mockUpdate({ set: v, where: w }), + }), + }), }, })) @@ -43,6 +51,8 @@ jest.mock('@/lib/logger', () => ({ // --- Import after mocks --- import { POST } from '../preferences/route' +import { resident as residentTable } from '@/lib/db' +import { eq } from 'drizzle-orm' // --- Helpers --- @@ -95,7 +105,7 @@ describe('POST /api/portal/preferences', () => { test('returns 404 when resident code not found in DB', async () => { mockCookieGet.mockReturnValue({ value: 'INVALID-CODE' }) - mockFindUnique.mockResolvedValue(null) + mockFindFirst.mockResolvedValue(null) const req = createPreferencesRequest(VALID_PREFS) const res = await POST(req) @@ -109,7 +119,7 @@ describe('POST /api/portal/preferences', () => { test('updates preferences and returns success', async () => { const resident = { id: 'res-1', code: 'RES-001' } mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(resident) + mockFindFirst.mockResolvedValue(resident) mockUpdate.mockResolvedValue(resident) const req = createPreferencesRequest(VALID_PREFS) @@ -119,10 +129,9 @@ describe('POST /api/portal/preferences', () => { expect(res.status).toBe(200) expect(body.success).toBe(true) - // Verify prisma.resident.update was called with correct data + // Verify the resident update was called with correct data expect(mockUpdate).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: expect.objectContaining({ + set: expect.objectContaining({ sleepSchedule: 'STANDARD', noiseTolerance: 3, cleanlinessPractice: 4, @@ -130,13 +139,14 @@ describe('POST /api/portal/preferences', () => { privacyNeed: 3, smokingStatus: 'NON_SMOKER', }), + where: eq(residentTable.id, 'res-1'), }) }) test('builds roommatePreferences text from optional fields', async () => { const resident = { id: 'res-2', code: 'RES-002' } mockCookieGet.mockReturnValue({ value: 'RES-002' }) - mockFindUnique.mockResolvedValue(resident) + mockFindFirst.mockResolvedValue(resident) mockUpdate.mockResolvedValue(resident) const prefsWithRoommate = { @@ -154,14 +164,14 @@ describe('POST /api/portal/preferences', () => { expect(body.success).toBe(true) const updateCall = mockUpdate.mock.calls[0][0] - expect(updateCall.data.roommatePreferences).toContain('Altersgruppe: 25-35') - expect(updateCall.data.roommatePreferences).toContain('Kultur: Arabisch') - expect(updateCall.data.roommatePreferences).toContain('Ruhige Person') + expect(updateCall.set.roommatePreferences).toContain('Altersgruppe: 25-35') + expect(updateCall.set.roommatePreferences).toContain('Kultur: Arabisch') + expect(updateCall.set.roommatePreferences).toContain('Ruhige Person') }) test('returns 400 on validation error (invalid sleepSchedule)', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + mockFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) const invalidPrefs = { ...VALID_PREFS, sleepSchedule: 'INVALID_VALUE' } const req = createPreferencesRequest(invalidPrefs) @@ -174,7 +184,7 @@ describe('POST /api/portal/preferences', () => { test('returns 500 when database update fails', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + mockFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) mockUpdate.mockRejectedValue(new Error('DB connection failed')) const req = createPreferencesRequest(VALID_PREFS) @@ -193,7 +203,7 @@ describe('POST /api/portal/preferences', () => { test('always updates the resident identified by cookie, never request body', async () => { const ownResident = { id: 'res-own', code: 'RES-OWN' } mockCookieGet.mockReturnValue({ value: 'RES-OWN' }) - mockFindUnique.mockResolvedValue(ownResident) + mockFindFirst.mockResolvedValue(ownResident) mockUpdate.mockResolvedValue(ownResident) const req = createPreferencesRequest(VALID_PREFS) @@ -204,25 +214,31 @@ describe('POST /api/portal/preferences', () => { expect(body.success).toBe(true) // Must use DB-resolved resident id, not any value from request body - expect(mockFindUnique).toHaveBeenCalledWith( - expect.objectContaining({ where: { code: 'RES-OWN' } }), + expect(mockFindFirst).toHaveBeenCalledWith( + expect.objectContaining({ where: eq(residentTable.code, 'RES-OWN') }), + ) + expect(mockUpdate).toHaveBeenCalledWith( + expect.objectContaining({ where: eq(residentTable.id, 'res-own') }), ) - expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ where: { id: 'res-own' } })) }) test('cannot update another resident by cookie swap — only cookie-identified resident is updated', async () => { // Resident A has their own cookie const residentA = { id: 'res-a', code: 'RES-AAA' } mockCookieGet.mockReturnValue({ value: 'RES-AAA' }) - mockFindUnique.mockResolvedValue(residentA) + mockFindFirst.mockResolvedValue(residentA) mockUpdate.mockResolvedValue(residentA) const req = createPreferencesRequest(VALID_PREFS) await POST(req) // Update is scoped to resident A's id, regardless of anything else - expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ where: { id: 'res-a' } })) + expect(mockUpdate).toHaveBeenCalledWith( + expect.objectContaining({ where: eq(residentTable.id, 'res-a') }), + ) // Resident B's id never appears in any DB call - expect(mockUpdate).not.toHaveBeenCalledWith(expect.objectContaining({ where: { id: 'res-b' } })) + expect(mockUpdate).not.toHaveBeenCalledWith( + expect.objectContaining({ where: eq(residentTable.id, 'res-b') }), + ) }) }) diff --git a/src/app/api/portal/__tests__/profile.test.ts b/src/app/api/portal/__tests__/profile.test.ts index ef00089c..0b873f2a 100644 --- a/src/app/api/portal/__tests__/profile.test.ts +++ b/src/app/api/portal/__tests__/profile.test.ts @@ -31,27 +31,54 @@ jest.mock('@/lib/auth', () => ({ const mockResidentUpdate = jest.fn() // Every photo request now loads the subject's visibility setting first. -const mockResidentFindUnique = jest.fn() +const mockResidentFindFirst = jest.fn() const mockPhotoUpsert = jest.fn() const mockPhotoDeleteMany = jest.fn() -const mockPhotoFindUnique = jest.fn() +const mockPhotoFindFirst = jest.fn() const mockPlacementFindFirst = jest.fn() const mockUnitUpdate = jest.fn() -jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - update: (...args: unknown[]) => mockResidentUpdate(...args), - findUnique: (...args: unknown[]) => mockResidentFindUnique(...args), +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + return { + ...actual, + db: { + query: { + resident: { findFirst: (...args: unknown[]) => mockResidentFindFirst(...args) }, + residentPhoto: { findFirst: (...args: unknown[]) => mockPhotoFindFirst(...args) }, + placement: { findFirst: (...args: unknown[]) => mockPlacementFindFirst(...args) }, + }, + // Profile PATCH updates Resident; apartment PATCH updates HousingUnit. + // Both mocks receive { set, where } and resolve the .returning() array. + update: (table: unknown) => ({ + set: (v: unknown) => ({ + where: (w: unknown) => ({ + returning: (): Promise => + table === actual.resident + ? mockResidentUpdate({ set: v, where: w }) + : mockUnitUpdate({ set: v, where: w }), + }), + }), + }), + // Photo upload is insert…onConflictDoUpdate (an upsert). + insert: () => ({ + values: (v: unknown) => ({ + onConflictDoUpdate: (conflict: unknown): Promise => + mockPhotoUpsert({ values: v, conflict }), + }), + }), + delete: () => ({ where: (w: unknown): Promise => mockPhotoDeleteMany(w) }), + // The photo route builds a "shares a unit" subquery from db.select(); + // it is never awaited, only embedded into inArray(), so a plain + // chainable stub is enough. + select: () => ({ + from: () => { + const chain = { where: () => chain } + return chain + }, + }), }, - residentPhoto: { - upsert: (...args: unknown[]) => mockPhotoUpsert(...args), - deleteMany: (...args: unknown[]) => mockPhotoDeleteMany(...args), - findUnique: (...args: unknown[]) => mockPhotoFindUnique(...args), - }, - placement: { findFirst: (...args: unknown[]) => mockPlacementFindFirst(...args) }, - housingUnit: { update: (...args: unknown[]) => mockUnitUpdate(...args) }, - }, -})) + } +}) const mockLogAudit = jest.fn().mockResolvedValue(undefined) jest.mock('@/lib/audit', () => ({ @@ -74,6 +101,9 @@ import { PATCH as patchProfile } from '../profile/route' import { POST as uploadPhoto, DELETE as deletePhoto } from '../profile/photo/route' import { GET as getPhoto } from '../residents/[id]/photo/route' import { PATCH as patchApartment } from '../apartment/route' +import { housingUnit, residentPhoto } from '@/lib/db' +import { eq } from 'drizzle-orm' +import { PgDialect } from 'drizzle-orm/pg-core' // --- Helpers --- @@ -105,15 +135,17 @@ beforeEach(() => { // Default subject: the ROOMMATES setting, which is what the route enforced // before the setting existed — so these cases keep testing the old rule. // Echoes the id that was asked for, the way the real query does — a fixed id - // would make "am I looking at my own profile" false for everyone. - mockResidentFindUnique.mockImplementation((args: { where: { id: string } }) => - Promise.resolve({ id: args.where.id, profileVisibility: 'ROOMMATES' }), - ) - mockResidentUpdate.mockImplementation((args: { data: unknown }) => - Promise.resolve({ id: 'georgy', code: 'RES-GEO001', ...(args.data as object) }), + // would make "am I looking at my own profile" false for everyone. The id is + // the bound parameter of the drizzle eq() expression. + mockResidentFindFirst.mockImplementation((args: { where: never }) => { + const id = new PgDialect().sqlToQuery(args.where).params[0] as string + return Promise.resolve({ id, profileVisibility: 'ROOMMATES' }) + }) + mockResidentUpdate.mockImplementation((args: { set: unknown }) => + Promise.resolve([{ id: 'georgy', code: 'RES-GEO001', ...(args.set as object) }]), ) - mockUnitUpdate.mockImplementation((args: { data: { nickname: string | null } }) => - Promise.resolve({ id: 'unit-1', nickname: args.data.nickname }), + mockUnitUpdate.mockImplementation((args: { set: { nickname: string | null } }) => + Promise.resolve([{ id: 'unit-1', nickname: args.set.nickname }]), ) }) @@ -131,20 +163,27 @@ describe('PATCH /api/portal/profile', () => { jsonRequest('/api/portal/profile', { displayName: ' Georgy ', bio: 'Zimmer 1' }), ) expect(response.status).toBe(200) - expect(mockResidentUpdate.mock.calls[0][0].data).toEqual({ + expect(mockResidentUpdate.mock.calls[0][0].set).toEqual({ displayName: 'Georgy', bio: 'Zimmer 1', + updatedAt: expect.any(Date), }) }) it('clears a field when the empty string is sent', async () => { await patchProfile(jsonRequest('/api/portal/profile', { displayName: '' })) - expect(mockResidentUpdate.mock.calls[0][0].data).toEqual({ displayName: null }) + expect(mockResidentUpdate.mock.calls[0][0].set).toEqual({ + displayName: null, + updatedAt: expect.any(Date), + }) }) it('leaves untouched fields out of the update', async () => { await patchProfile(jsonRequest('/api/portal/profile', { bio: 'Nur Bio' })) - expect(mockResidentUpdate.mock.calls[0][0].data).toEqual({ bio: 'Nur Bio' }) + expect(mockResidentUpdate.mock.calls[0][0].set).toEqual({ + bio: 'Nur Bio', + updatedAt: expect.any(Date), + }) }) it('rejects an over-long displayName', async () => { @@ -162,8 +201,11 @@ describe('POST /api/portal/profile/photo', () => { const response = await uploadPhoto(photoRequest(file)) expect(response.status).toBe(200) const upsert = mockPhotoUpsert.mock.calls[0][0] - expect(upsert.where).toEqual({ residentId: 'georgy' }) - expect(upsert.create.mimeType).toBe('image/jpeg') + expect(upsert.values.residentId).toBe('georgy') + expect(upsert.values.mimeType).toBe('image/jpeg') + // Conflict on residentId makes the insert an upsert per resident. + expect(upsert.conflict.target).toBe(residentPhoto.residentId) + expect(upsert.conflict.set.mimeType).toBe('image/jpeg') }) it('rejects a disallowed mime type', async () => { @@ -193,7 +235,7 @@ describe('POST /api/portal/profile/photo', () => { it('DELETE removes the photo', async () => { const response = await deletePhoto() expect(response.status).toBe(200) - expect(mockPhotoDeleteMany).toHaveBeenCalledWith({ where: { residentId: 'georgy' } }) + expect(mockPhotoDeleteMany).toHaveBeenCalledWith(eq(residentPhoto.residentId, 'georgy')) }) }) @@ -207,7 +249,7 @@ describe('GET /api/portal/residents/[id]/photo', () => { } it('serves your own photo without a placement check', async () => { - mockPhotoFindUnique.mockResolvedValue({ ...PHOTO, residentId: 'georgy' }) + mockPhotoFindFirst.mockResolvedValue({ ...PHOTO, residentId: 'georgy' }) const response = await get('georgy') expect(response.status).toBe(200) expect(response.headers.get('Content-Type')).toBe('image/jpeg') @@ -219,8 +261,8 @@ describe('GET /api/portal/residents/[id]/photo', () => { // deliberate change from the route's original behaviour, which hid photos // from staff too. mockGetCurrentUser.mockResolvedValue({ id: 'staff-1', role: 'ADMIN' }) - mockResidentFindUnique.mockResolvedValue({ id: 'ihor', profileVisibility: 'PRIVATE' }) - mockPhotoFindUnique.mockResolvedValue(PHOTO) + mockResidentFindFirst.mockResolvedValue({ id: 'ihor', profileVisibility: 'PRIVATE' }) + mockPhotoFindFirst.mockResolvedValue(PHOTO) const response = await get('ihor') @@ -230,17 +272,17 @@ describe('GET /api/portal/residents/[id]/photo', () => { }) it('404s for a roommate when the resident chose PRIVATE', async () => { - mockResidentFindUnique.mockResolvedValue({ id: 'ihor', profileVisibility: 'PRIVATE' }) + mockResidentFindFirst.mockResolvedValue({ id: 'ihor', profileVisibility: 'PRIVATE' }) mockPlacementFindFirst.mockResolvedValue({ id: 'placement-1' }) - mockPhotoFindUnique.mockResolvedValue(PHOTO) + mockPhotoFindFirst.mockResolvedValue(PHOTO) expect((await get('ihor')).status).toBe(404) }) it('serves a resident of another unit when the setting is RESIDENTS', async () => { - mockResidentFindUnique.mockResolvedValue({ id: 'ihor', profileVisibility: 'RESIDENTS' }) + mockResidentFindFirst.mockResolvedValue({ id: 'ihor', profileVisibility: 'RESIDENTS' }) mockPlacementFindFirst.mockResolvedValue(null) - mockPhotoFindUnique.mockResolvedValue(PHOTO) + mockPhotoFindFirst.mockResolvedValue(PHOTO) expect((await get('ihor')).status).toBe(200) }) @@ -248,15 +290,15 @@ describe('GET /api/portal/residents/[id]/photo', () => { it('404s for an unknown resident without touching the photo table', async () => { // Ordering matters: looking up the photo first would answer "does this // person have a picture" for an id the caller may simply have guessed. - mockResidentFindUnique.mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) expect((await get('nobody')).status).toBe(404) - expect(mockPhotoFindUnique).not.toHaveBeenCalled() + expect(mockPhotoFindFirst).not.toHaveBeenCalled() }) it("serves a roommate's photo when a shared unit exists", async () => { mockPlacementFindFirst.mockResolvedValue({ id: 'placement-1' }) - mockPhotoFindUnique.mockResolvedValue(PHOTO) + mockPhotoFindFirst.mockResolvedValue(PHOTO) const response = await get('ihor') expect(response.status).toBe(200) }) @@ -265,12 +307,12 @@ describe('GET /api/portal/residents/[id]/photo', () => { mockPlacementFindFirst.mockResolvedValue(null) const response = await get('ihor') expect(response.status).toBe(404) - expect(mockPhotoFindUnique).not.toHaveBeenCalled() + expect(mockPhotoFindFirst).not.toHaveBeenCalled() }) it('404s when the roommate has no photo', async () => { mockPlacementFindFirst.mockResolvedValue({ id: 'placement-1' }) - mockPhotoFindUnique.mockResolvedValue(null) + mockPhotoFindFirst.mockResolvedValue(null) const response = await get('ihor') expect(response.status).toBe(404) }) @@ -283,14 +325,14 @@ describe('PATCH /api/portal/apartment', () => { ) expect(response.status).toBe(200) expect(mockUnitUpdate.mock.calls[0][0]).toMatchObject({ - where: { id: 'unit-1' }, - data: { nickname: 'Singapur' }, + set: { nickname: 'Singapur' }, + where: eq(housingUnit.id, 'unit-1'), }) }) it('clears the nickname with an empty string', async () => { await patchApartment(jsonRequest('/api/portal/apartment', { nickname: '' })) - expect(mockUnitUpdate.mock.calls[0][0].data).toEqual({ nickname: null }) + expect(mockUnitUpdate.mock.calls[0][0].set).toEqual({ nickname: null }) }) it('returns 401 without a placement-backed session', async () => { diff --git a/src/app/api/portal/__tests__/report.test.ts b/src/app/api/portal/__tests__/report.test.ts index 9456e559..cebdc837 100644 --- a/src/app/api/portal/__tests__/report.test.ts +++ b/src/app/api/portal/__tests__/report.test.ts @@ -17,26 +17,34 @@ jest.mock('next/headers', () => ({ }), })) -const mockFindUnique = jest.fn() +const mockFindFirst = jest.fn() const mockIncidentCreate = jest.fn() const mockMaintenanceCreate = jest.fn() const mockPlacementFindFirst = jest.fn() -jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findUnique: (...args: unknown[]) => mockFindUnique(...args), - }, - incident: { - create: (...args: unknown[]) => mockIncidentCreate(...args), - }, - maintenanceRequest: { - create: (...args: unknown[]) => mockMaintenanceCreate(...args), - }, - placement: { - findFirst: (...args: unknown[]) => mockPlacementFindFirst(...args), +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + return { + ...actual, + db: { + query: { + resident: { + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, + placement: { + findFirst: (...args: unknown[]) => mockPlacementFindFirst(...args), + }, + }, + // Both report paths are insert(...).values(...).returning(); dispatch by + // table so incidents and maintenance requests stay separately assertable. + insert: (table: unknown) => ({ + values: (v: unknown) => ({ + returning: (): Promise => + table === actual.incident ? mockIncidentCreate(v) : mockMaintenanceCreate(v), + }), + }), }, - }, -})) + } +}) const mockLogAudit = jest.fn().mockResolvedValue(undefined) jest.mock('@/lib/audit', () => ({ @@ -151,12 +159,12 @@ describe('POST /api/portal/report', () => { expect(res.status).toBe(401) expect(body.success).toBe(false) expect(body.error).toBe(ERROR_MESSAGES.NOT_AUTHENTICATED) - expect(mockFindUnique).not.toHaveBeenCalled() + expect(mockFindFirst).not.toHaveBeenCalled() }) test('returns 404 when resident not found', async () => { mockCookieGet.mockReturnValue({ value: 'UNKNOWN-CODE' }) - mockFindUnique.mockResolvedValue(null) + mockFindFirst.mockResolvedValue(null) const req = createFormDataRequest({ description: 'test' }) const res = await POST(req) @@ -169,7 +177,7 @@ describe('POST /api/portal/report', () => { test('returns 400 when no active placement', async () => { mockCookieGet.mockReturnValue({ value: 'RES-002' }) - mockFindUnique.mockResolvedValue(RESIDENT_NO_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_NO_PLACEMENT) const req = createFormDataRequest({ description: 'test' }) const res = await POST(req) @@ -182,7 +190,7 @@ describe('POST /api/portal/report', () => { test('returns 400 when validation fails with ValidationError', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockImplementation(() => { throw new MockValidationError('Beschreibung ist erforderlich') }) @@ -198,7 +206,7 @@ describe('POST /api/portal/report', () => { test('returns 400 with generic message for non-validation errors during parsing', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockImplementation(() => { throw new Error('Unexpected parse failure') }) @@ -217,7 +225,7 @@ describe('POST /api/portal/report', () => { // Incident left the report invisible to the people who fix things, and // inflated the incident count AOZ uses to measure conflict. mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'MAINTENANCE', type: 'PLUMBING', @@ -226,12 +234,9 @@ describe('POST /api/portal/report', () => { location: 'kitchen', incidentDate: null, }) - mockMaintenanceCreate.mockResolvedValue({ - id: 'mr-1', - category: 'PLUMBING', - priority: 'NORMAL', - title: 'Sanitär', - }) + mockMaintenanceCreate.mockResolvedValue([ + { id: 'mr-1', category: 'PLUMBING', priority: 'NORMAL', title: 'Sanitär' }, + ]) const req = createFormDataRequest({ description: 'Wasserhahn tropft' }) const res = await POST(req) @@ -241,8 +246,8 @@ describe('POST /api/portal/report', () => { expect(body.success).toBe(true) expect(mockIncidentCreate).not.toHaveBeenCalled() - expect(mockMaintenanceCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockMaintenanceCreate).toHaveBeenCalledWith( + expect.objectContaining({ housingUnitId: 'hu-1', reportedById: 'res-1', category: 'PLUMBING', @@ -252,12 +257,12 @@ describe('POST /api/portal/report', () => { description: 'Wasserhahn tropft', location: 'Küche', }), - }) + ) }) test("severity maps onto the maintenance board's own urgency scale", async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'MAINTENANCE', type: 'PLUMBING', @@ -266,21 +271,18 @@ describe('POST /api/portal/report', () => { location: 'bathroom', incidentDate: null, }) - mockMaintenanceCreate.mockResolvedValue({ - id: 'mr-2', - category: 'PLUMBING', - priority: 'URGENT', - title: 'Sanitär', - }) + mockMaintenanceCreate.mockResolvedValue([ + { id: 'mr-2', category: 'PLUMBING', priority: 'URGENT', title: 'Sanitär' }, + ]) await POST(createFormDataRequest({ description: 'x' })) - expect(mockMaintenanceCreate.mock.calls[0][0].data.priority).toBe('URGENT') + expect(mockMaintenanceCreate.mock.calls[0][0].priority).toBe('URGENT') }) test('notifies staff about maintenance as a request, not as a Vorfall', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'MAINTENANCE', type: 'PLUMBING', @@ -289,12 +291,9 @@ describe('POST /api/portal/report', () => { location: 'kitchen', incidentDate: null, }) - mockMaintenanceCreate.mockResolvedValue({ - id: 'mr-3', - category: 'PLUMBING', - priority: 'NORMAL', - title: 'Sanitär', - }) + mockMaintenanceCreate.mockResolvedValue([ + { id: 'mr-3', category: 'PLUMBING', priority: 'NORMAL', title: 'Sanitär' }, + ]) await POST(createFormDataRequest({ description: 'x' })) @@ -305,7 +304,7 @@ describe('POST /api/portal/report', () => { test('creates interpersonal incident with mediation note appended', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'INTERPERSONAL', type: 'NOISE_COMPLAINT', @@ -315,7 +314,7 @@ describe('POST /api/portal/report', () => { involvedResident: 'external', incidentDate: null, }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-2' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-2' }]) const req = createFormDataRequest({ description: 'Laute Musik nach 22 Uhr' }) const res = await POST(req) @@ -326,16 +325,16 @@ describe('POST /api/portal/report', () => { // Description should have mediation note appended const createCall = mockIncidentCreate.mock.calls[0][0] - expect(createCall.data.description).toContain('Laute Musik nach 22 Uhr') - expect(createCall.data.description).toContain('[Bewohner wünscht Vermittlungsgespräch]') + expect(createCall.description).toContain('Laute Musik nach 22 Uhr') + expect(createCall.description).toContain('[Bewohner wünscht Vermittlungsgespräch]') // involvedResident === 'external' should result in null subjectId - expect(createCall.data.subjectId).toBeNull() + expect(createCall.subjectId).toBeNull() }) test('sets subjectId when involvedResident is a roommate in the same unit', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) // Validation lookup: res-99 IS in the same unit, so insert proceeds. mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-99' }) mockValidateFormData.mockReturnValue({ @@ -347,18 +346,18 @@ describe('POST /api/portal/report', () => { requestMediation: false, incidentDate: null, }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-3' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-3' }]) const req = createFormDataRequest({ description: 'Streit' }) await POST(req) const createCall = mockIncidentCreate.mock.calls[0][0] - expect(createCall.data.subjectId).toBe('res-99') + expect(createCall.subjectId).toBe('res-99') }) test('rejects involvedResident that does not live in the same unit', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) // Validation lookup: stranger NOT in this unit → null. mockPlacementFindFirst.mockResolvedValue(null) mockValidateFormData.mockReturnValue({ @@ -380,7 +379,7 @@ describe('POST /api/portal/report', () => { test('uses provided incidentDate when present', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'INTERPERSONAL', type: 'SCHEDULE_CONFLICT', @@ -390,18 +389,18 @@ describe('POST /api/portal/report', () => { requestMediation: false, incidentDate: '2026-01-15', }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-4' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-4' }]) const req = createFormDataRequest({ description: 'Streit um Duschzeiten' }) await POST(req) const createCall = mockIncidentCreate.mock.calls[0][0] - expect(createCall.data.date).toEqual(new Date('2026-01-15')) + expect(createCall.date).toEqual(new Date('2026-01-15')) }) test('calls logAudit with correct data after incident creation', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'INTERPERSONAL', type: 'SPACE_DISPUTE', @@ -411,7 +410,7 @@ describe('POST /api/portal/report', () => { requestMediation: false, incidentDate: null, }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-audit' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-audit' }]) const req = createFormDataRequest({ description: 'Streit um den Kühlschrank' }) await POST(req) @@ -434,7 +433,7 @@ describe('POST /api/portal/report', () => { test('audits a maintenance report against the maintenance entity', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'MAINTENANCE', type: 'PLUMBING', @@ -444,12 +443,9 @@ describe('POST /api/portal/report', () => { requestMediation: false, incidentDate: null, }) - mockMaintenanceCreate.mockResolvedValue({ - id: 'mr-audit', - category: 'PLUMBING', - priority: 'HIGH', - title: 'Sanitär', - }) + mockMaintenanceCreate.mockResolvedValue([ + { id: 'mr-audit', category: 'PLUMBING', priority: 'HIGH', title: 'Sanitär' }, + ]) await POST(createFormDataRequest({ description: 'Rohrbruch' })) @@ -468,7 +464,7 @@ describe('POST /api/portal/report', () => { test('sends staff notification after successful incident creation', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'INTERPERSONAL', type: 'NOISE_COMPLAINT', @@ -478,7 +474,7 @@ describe('POST /api/portal/report', () => { involvedResident: 'external', incidentDate: null, }) - mockIncidentCreate.mockResolvedValue({ id: 'inc-notify' }) + mockIncidentCreate.mockResolvedValue([{ id: 'inc-notify' }]) const req = createFormDataRequest({ description: 'Laute Musik' }) await POST(req) @@ -499,9 +495,9 @@ describe('POST /api/portal/report', () => { expect(mockNotifyStaff).toHaveBeenCalledWith('[AOZ Housing] Neuer Vorfall', '

    test

    ') }) - test('returns 500 when prisma.incident.create fails', async () => { + test('returns 500 when the incident insert fails', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'INTERPERSONAL', type: 'CULTURAL_FRICTION', @@ -525,7 +521,7 @@ describe('POST /api/portal/report', () => { test('falls back to location value when label not found in PORTAL_LABELS', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockValidateFormData.mockReturnValue({ category: 'MAINTENANCE', type: 'GENERAL_MAINTENANCE', @@ -534,17 +530,14 @@ describe('POST /api/portal/report', () => { location: 'basement', // not in the locations array incidentDate: null, }) - mockMaintenanceCreate.mockResolvedValue({ - id: 'mr-5', - category: 'OTHER', - priority: 'LOW', - title: 'Allgemeine Wartung', - }) + mockMaintenanceCreate.mockResolvedValue([ + { id: 'mr-5', category: 'OTHER', priority: 'LOW', title: 'Allgemeine Wartung' }, + ]) const req = createFormDataRequest({ description: 'Problem im Keller' }) await POST(req) // Falls back to the raw value when no matching label - expect(mockMaintenanceCreate.mock.calls[0][0].data.location).toBe('basement') + expect(mockMaintenanceCreate.mock.calls[0][0].location).toBe('basement') }) }) diff --git a/src/app/api/portal/__tests__/satisfaction.test.ts b/src/app/api/portal/__tests__/satisfaction.test.ts index 38acd6d7..f308ae7f 100644 --- a/src/app/api/portal/__tests__/satisfaction.test.ts +++ b/src/app/api/portal/__tests__/satisfaction.test.ts @@ -17,24 +17,38 @@ jest.mock('next/headers', () => ({ }), })) -// Transaction mock objects — these are used inside the $transaction callback +// Transaction mock objects — these are used inside the db.transaction callback. +// The tx inserts the check-in and (for low ratings) an incident — both awaited +// on .values() — and updates the placement via .set().where(). Create mocks +// receive the values payload; the update mock receives { set, where }. const mockTxSatisfactionCheckInCreate = jest.fn() const mockTxPlacementUpdate = jest.fn() const mockTxIncidentCreate = jest.fn() const tx = { - satisfactionCheckIn: { create: (...args: unknown[]) => mockTxSatisfactionCheckInCreate(...args) }, - placement: { update: (...args: unknown[]) => mockTxPlacementUpdate(...args) }, - incident: { create: (...args: unknown[]) => mockTxIncidentCreate(...args) }, + insert: (table: unknown) => ({ + values: (v: unknown): Promise => + table === satisfactionCheckInTable + ? mockTxSatisfactionCheckInCreate(v) + : mockTxIncidentCreate(v), + }), + update: () => ({ + set: (v: unknown) => ({ + where: (w: unknown): Promise => mockTxPlacementUpdate({ set: v, where: w }), + }), + }), } -const mockFindUnique = jest.fn() +const mockFindFirst = jest.fn() const mockTransaction = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findUnique: (...args: unknown[]) => mockFindUnique(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, }, - $transaction: (...args: unknown[]) => mockTransaction(...args), + transaction: (...args: unknown[]) => mockTransaction(...args), }, })) @@ -62,6 +76,11 @@ jest.mock('@/lib/email', () => ({ // --- Import after mocks --- import { POST, GET } from '../satisfaction/route' +import { + placement as placementTable, + satisfactionCheckIn as satisfactionCheckInTable, +} from '@/lib/db' +import { eq } from 'drizzle-orm' // --- Helpers --- @@ -118,7 +137,7 @@ describe('POST /api/portal/satisfaction', () => { expect(res.status).toBe(401) expect(body.success).toBe(false) expect(body.error).toBe(ERROR_MESSAGES.NOT_AUTHENTICATED) - expect(mockFindUnique).not.toHaveBeenCalled() + expect(mockFindFirst).not.toHaveBeenCalled() }) test('returns 400 when validation fails', async () => { @@ -132,13 +151,13 @@ describe('POST /api/portal/satisfaction', () => { expect(res.status).toBe(400) expect(body.success).toBe(false) expect(body.error).toBe(ERROR_MESSAGES.INVALID_RATING) - expect(mockFindUnique).not.toHaveBeenCalled() + expect(mockFindFirst).not.toHaveBeenCalled() }) test('returns 404 when resident not found', async () => { mockCookieGet.mockReturnValue({ value: 'UNKNOWN' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 4, concerns: null } }) - mockFindUnique.mockResolvedValue(null) + mockFindFirst.mockResolvedValue(null) const req = createJsonRequest({ rating: 4 }) const res = await POST(req) @@ -152,7 +171,7 @@ describe('POST /api/portal/satisfaction', () => { test('returns 400 when no active placement', async () => { mockCookieGet.mockReturnValue({ value: 'RES-002' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 3, concerns: null } }) - mockFindUnique.mockResolvedValue(RESIDENT_NO_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_NO_PLACEMENT) const req = createJsonRequest({ rating: 3 }) const res = await POST(req) @@ -166,7 +185,7 @@ describe('POST /api/portal/satisfaction', () => { test('saves rating and returns success with rating value', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 4, concerns: null } }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const req = createJsonRequest({ rating: 4 }) const res = await POST(req) @@ -176,20 +195,20 @@ describe('POST /api/portal/satisfaction', () => { expect(body.success).toBe(true) expect(body.rating).toBe(4) - // Verify satisfactionCheckIn.create was called - expect(mockTxSatisfactionCheckInCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + // Verify the check-in insert was called + expect(mockTxSatisfactionCheckInCreate).toHaveBeenCalledWith( + expect.objectContaining({ placementId: 'pl-1', checkInType: 'AD_HOC', overallSatisfaction: 4, isAnonymous: true, }), - }) + ) - // Verify placement.update was called + // Verify the placement update was called expect(mockTxPlacementUpdate).toHaveBeenCalledWith({ - where: { id: 'pl-1' }, - data: { satisfactionRating: 4 }, + set: { satisfactionRating: 4 }, + where: eq(placementTable.id, 'pl-1'), }) // Should NOT create an incident for rating > 2 @@ -199,7 +218,7 @@ describe('POST /api/portal/satisfaction', () => { test('creates incident for low rating (rating = 2)', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 2, concerns: 'Lärm nachts' } }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const req = createJsonRequest({ rating: 2, concerns: 'Lärm nachts' }) const res = await POST(req) @@ -210,25 +229,25 @@ describe('POST /api/portal/satisfaction', () => { expect(body.rating).toBe(2) // Should create an incident with MEDIUM severity for rating 2 - expect(mockTxIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockTxIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ housingUnitId: 'hu-1', reportedById: 'res-1', category: 'WELLBEING', type: 'LOW_SATISFACTION', severity: 'MEDIUM', }), - }) + ) // Description should include the concerns text - const incidentData = mockTxIncidentCreate.mock.calls[0][0].data + const incidentData = mockTxIncidentCreate.mock.calls[0][0] expect(incidentData.description).toContain('Lärm nachts') }) test('creates incident with HIGH severity for rating = 1', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 1, concerns: null } }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const req = createJsonRequest({ rating: 1 }) const res = await POST(req) @@ -237,23 +256,23 @@ describe('POST /api/portal/satisfaction', () => { expect(res.status).toBe(200) expect(body.success).toBe(true) - expect(mockTxIncidentCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockTxIncidentCreate).toHaveBeenCalledWith( + expect.objectContaining({ severity: 'HIGH', category: 'WELLBEING', type: 'LOW_SATISFACTION', }), - }) + ) // No concerns provided — should use fallback description - const incidentData = mockTxIncidentCreate.mock.calls[0][0].data + const incidentData = mockTxIncidentCreate.mock.calls[0][0] expect(incidentData.description).toContain('keine Details angegeben') }) test('does not create incident for rating = 3', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 3, concerns: null } }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const req = createJsonRequest({ rating: 3 }) await POST(req) @@ -267,19 +286,19 @@ describe('POST /api/portal/satisfaction', () => { success: true, data: { rating: 4, concerns: 'Küche ist oft schmutzig' }, }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const req = createJsonRequest({ rating: 4, concerns: 'Küche ist oft schmutzig' }) await POST(req) - const checkInData = mockTxSatisfactionCheckInCreate.mock.calls[0][0].data + const checkInData = mockTxSatisfactionCheckInCreate.mock.calls[0][0] expect(checkInData.concerns).toBe('Küche ist oft schmutzig') }) test('returns 500 on database transaction error', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) mockSafeParse.mockReturnValue({ success: true, data: { rating: 5, concerns: null } }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockTransaction.mockRejectedValue(new Error('Transaction failed')) const req = createJsonRequest({ rating: 5 }) @@ -312,7 +331,7 @@ describe('GET /api/portal/satisfaction', () => { test('returns null values when resident not found', async () => { mockCookieGet.mockReturnValue({ value: 'UNKNOWN' }) - mockFindUnique.mockResolvedValue(null) + mockFindFirst.mockResolvedValue(null) const res = await GET() const body = await res.json() @@ -324,7 +343,7 @@ describe('GET /api/portal/satisfaction', () => { test('returns null values when no active placement', async () => { mockCookieGet.mockReturnValue({ value: 'RES-002' }) - mockFindUnique.mockResolvedValue(RESIDENT_NO_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_NO_PLACEMENT) const res = await GET() const body = await res.json() @@ -336,7 +355,7 @@ describe('GET /api/portal/satisfaction', () => { test('returns lastCheckIn and rating when data exists', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const res = await GET() const body = await res.json() @@ -348,7 +367,7 @@ describe('GET /api/portal/satisfaction', () => { test('returns null lastCheckIn when no check-ins exist', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001', placements: [ diff --git a/src/app/api/portal/__tests__/transfer.test.ts b/src/app/api/portal/__tests__/transfer.test.ts index 2eb35fbf..9084ff39 100644 --- a/src/app/api/portal/__tests__/transfer.test.ts +++ b/src/app/api/portal/__tests__/transfer.test.ts @@ -17,20 +17,25 @@ jest.mock('next/headers', () => ({ }), })) -const mockFindUnique = jest.fn() +const mockFindFirst = jest.fn() const mockTransferCreate = jest.fn() -const mockHousingUnitFindUnique = jest.fn() +const mockHousingUnitFindFirst = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findUnique: (...args: unknown[]) => mockFindUnique(...args), - }, - transferRequest: { - create: (...args: unknown[]) => mockTransferCreate(...args), - }, - housingUnit: { - findUnique: (...args: unknown[]) => mockHousingUnitFindUnique(...args), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { + findFirst: (...args: unknown[]) => mockFindFirst(...args), + }, + housingUnit: { + findFirst: (...args: unknown[]) => mockHousingUnitFindFirst(...args), + }, }, + insert: jest.fn(() => ({ + values: (v: unknown) => ({ + returning: (): Promise => mockTransferCreate(v), + }), + })), }, })) @@ -112,7 +117,7 @@ describe('POST /api/portal/transfer', () => { expect(res.status).toBe(401) expect(body.success).toBe(false) expect(body.error).toBe(ERROR_MESSAGES.NOT_AUTHENTICATED) - expect(mockFindUnique).not.toHaveBeenCalled() + expect(mockFindFirst).not.toHaveBeenCalled() }) test('returns 400 for invalid input (reason too short)', async () => { @@ -129,7 +134,7 @@ describe('POST /api/portal/transfer', () => { test('returns 404 when resident not found', async () => { mockCookieGet.mockReturnValue({ value: 'UNKNOWN-CODE' }) - mockFindUnique.mockResolvedValue(null) + mockFindFirst.mockResolvedValue(null) const req = createTransferRequest({ reason: VALID_REASON }) const res = await POST(req) @@ -142,7 +147,7 @@ describe('POST /api/portal/transfer', () => { test('returns 400 when no active placement', async () => { mockCookieGet.mockReturnValue({ value: 'RES-003' }) - mockFindUnique.mockResolvedValue(RESIDENT_NO_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_NO_PLACEMENT) const req = createTransferRequest({ reason: VALID_REASON }) const res = await POST(req) @@ -155,7 +160,7 @@ describe('POST /api/portal/transfer', () => { test('returns 409 when pending request already exists', async () => { mockCookieGet.mockReturnValue({ value: 'RES-002' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PENDING_TRANSFER) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PENDING_TRANSFER) const req = createTransferRequest({ reason: VALID_REASON }) const res = await POST(req) @@ -168,9 +173,9 @@ describe('POST /api/portal/transfer', () => { test('returns 200 with success and id on happy path', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) - mockTransferCreate.mockResolvedValue({ id: 'tr-new' }) - mockHousingUnitFindUnique.mockResolvedValue({ code: 'WE-001' }) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockTransferCreate.mockResolvedValue([{ id: 'tr-new' }]) + mockHousingUnitFindFirst.mockResolvedValue({ code: 'WE-001' }) const req = createTransferRequest({ reason: VALID_REASON }) const res = await POST(req) @@ -181,20 +186,18 @@ describe('POST /api/portal/transfer', () => { expect(body.id).toBe('tr-new') expect(mockTransferCreate).toHaveBeenCalledWith({ - data: { - residentId: 'res-1', - currentPlacementId: 'pl-1', - targetUnitId: null, - reason: VALID_REASON, - }, + residentId: 'res-1', + currentPlacementId: 'pl-1', + targetUnitId: null, + reason: VALID_REASON, }) }) test('creates audit log on success', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) - mockTransferCreate.mockResolvedValue({ id: 'tr-audit' }) - mockHousingUnitFindUnique.mockResolvedValue({ code: 'WE-001' }) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockTransferCreate.mockResolvedValue([{ id: 'tr-audit' }]) + mockHousingUnitFindFirst.mockResolvedValue({ code: 'WE-001' }) const req = createTransferRequest({ reason: VALID_REASON }) await POST(req) @@ -209,7 +212,7 @@ describe('POST /api/portal/transfer', () => { test('returns 500 on database error', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) mockTransferCreate.mockRejectedValue(new Error('DB connection failed')) const req = createTransferRequest({ reason: VALID_REASON }) @@ -224,9 +227,9 @@ describe('POST /api/portal/transfer', () => { test('passes targetUnitId when provided', async () => { mockCookieGet.mockReturnValue({ value: 'RES-001' }) - mockFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) - mockTransferCreate.mockResolvedValue({ id: 'tr-target' }) - mockHousingUnitFindUnique.mockResolvedValue({ code: 'WE-002' }) + mockFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockTransferCreate.mockResolvedValue([{ id: 'tr-target' }]) + mockHousingUnitFindFirst.mockResolvedValue({ code: 'WE-002' }) const req = createTransferRequest({ reason: VALID_REASON, @@ -234,10 +237,10 @@ describe('POST /api/portal/transfer', () => { }) await POST(req) - expect(mockTransferCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockTransferCreate).toHaveBeenCalledWith( + expect.objectContaining({ targetUnitId: 'hu-target', }), - }) + ) }) }) diff --git a/src/components/matching/__tests__/MatchCard.test.tsx b/src/components/matching/__tests__/MatchCard.test.tsx index 8e4e616a..adedc7b0 100644 --- a/src/components/matching/__tests__/MatchCard.test.tsx +++ b/src/components/matching/__tests__/MatchCard.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' import { MatchCard } from '../MatchCard' import type { MatchResult } from '@/lib/matching/types' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' // ============================================================================= // MOCKS diff --git a/src/components/opportunities/OpportunityFormFields.tsx b/src/components/opportunities/OpportunityFormFields.tsx index 19f2bf56..69db92ba 100644 --- a/src/components/opportunities/OpportunityFormFields.tsx +++ b/src/components/opportunities/OpportunityFormFields.tsx @@ -4,7 +4,7 @@ * The "Voraussetzungen" block is the ethical core of the feature and is * grouped and captioned as such on purpose: everything in it describes the * PLACE. There is deliberately no field anywhere on this form that records - * anything about a person's status — see the note in `schema.prisma`. + * anything about a person’s status — see the note in src/lib/db/schema.ts. * * Every `name=` here is checked against the zod schema by * `opportunity-form-fields.test.ts`. A field the schema does not know is diff --git a/src/lib/__tests__/audit.test.ts b/src/lib/__tests__/audit.test.ts index 9c0bb670..a96004c4 100644 --- a/src/lib/__tests__/audit.test.ts +++ b/src/lib/__tests__/audit.test.ts @@ -6,6 +6,9 @@ */ import { logAudit, getEntityAuditLog, getRecentAuditLogs } from '../audit' +import { auditLog } from '@/lib/db' +import { desc } from 'drizzle-orm' +import { whereParts } from '@/test-utils/drizzle-where' // ============================================================================= // MOCKS @@ -15,10 +18,11 @@ const mockAuditLogCreate = jest.fn() const mockAuditLogFindMany = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - auditLog: { - create: (...args: unknown[]) => mockAuditLogCreate(...args), - findMany: (...args: unknown[]) => mockAuditLogFindMany(...args), + ...jest.requireActual('@/lib/db'), + db: { + insert: () => ({ values: (v: unknown) => Promise.resolve(mockAuditLogCreate(v)) }), + query: { + auditLog: { findMany: (...args: unknown[]) => mockAuditLogFindMany(...args) }, }, }, })) @@ -57,15 +61,15 @@ describe('logAudit', () => { reason: 'Test reason', }) - expect(mockAuditLogCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockAuditLogCreate).toHaveBeenCalledWith( + expect.objectContaining({ action: 'CREATE', entity: 'RESIDENT', entityId: 'res-123', userId: 'user-1', reason: 'Test reason', }), - }) + ) }) test('includes changes field when provided', async () => { @@ -79,9 +83,7 @@ describe('logAudit', () => { changes, }) - expect(mockAuditLogCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ changes }), - }) + expect(mockAuditLogCreate).toHaveBeenCalledWith(expect.objectContaining({ changes })) }) // ── Auto userId capture ─────────────────────────────────────────────────── @@ -95,9 +97,9 @@ describe('logAudit', () => { entityId: 'spot-456', }) - expect(mockAuditLogCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ userId: 'auto-user-id' }), - }) + expect(mockAuditLogCreate).toHaveBeenCalledWith( + expect.objectContaining({ userId: 'auto-user-id' }), + ) }) test('uses null userId when getCurrentUser returns null and userId not provided', async () => { @@ -109,9 +111,7 @@ describe('logAudit', () => { entityId: 'placement-789', }) - expect(mockAuditLogCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ userId: undefined }), - }) + expect(mockAuditLogCreate).toHaveBeenCalledWith(expect.objectContaining({ userId: undefined })) }) test('explicit userId takes precedence over getCurrentUser', async () => { @@ -126,14 +126,14 @@ describe('logAudit', () => { // getCurrentUser should NOT be called when userId is explicit expect(mockGetCurrentUser).not.toHaveBeenCalled() - expect(mockAuditLogCreate).toHaveBeenCalledWith({ - data: expect.objectContaining({ userId: 'explicit-user' }), - }) + expect(mockAuditLogCreate).toHaveBeenCalledWith( + expect.objectContaining({ userId: 'explicit-user' }), + ) }) // ── Non-blocking error handling ─────────────────────────────────────────── - test('does not throw when prisma.auditLog.create fails', async () => { + test('does not throw when the audit insert fails', async () => { mockAuditLogCreate.mockRejectedValue(new Error('DB connection lost')) await expect( @@ -175,11 +175,8 @@ describe('getEntityAuditLog', () => { test('queries by entity type and entityId', async () => { await getEntityAuditLog('RESIDENT', 'res-123') - expect(mockAuditLogFindMany).toHaveBeenCalledWith( - expect.objectContaining({ - where: { entity: 'RESIDENT', entityId: 'res-123' }, - }), - ) + const { where } = mockAuditLogFindMany.mock.calls[0][0] + expect(whereParts(where)).toEqual({ entity: 'RESIDENT', entityId: 'res-123' }) }) test('orders results by createdAt descending', async () => { @@ -187,7 +184,7 @@ describe('getEntityAuditLog', () => { expect(mockAuditLogFindMany).toHaveBeenCalledWith( expect.objectContaining({ - orderBy: { createdAt: 'desc' }, + orderBy: [desc(auditLog.createdAt)], }), ) }) @@ -195,10 +192,10 @@ describe('getEntityAuditLog', () => { test('limits results to 50', async () => { await getEntityAuditLog('INCIDENT', 'i-1') - expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ take: 50 })) + expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ limit: 50 })) }) - test('returns the results from prisma', async () => { + test('returns the rows the query hands back', async () => { const entries = [{ id: 'log-1', action: 'CREATE' }] mockAuditLogFindMany.mockResolvedValue(entries) @@ -221,7 +218,7 @@ describe('getRecentAuditLogs', () => { expect(mockAuditLogFindMany).toHaveBeenCalledWith( expect.objectContaining({ - orderBy: { createdAt: 'desc' }, + orderBy: [desc(auditLog.createdAt)], }), ) }) @@ -229,12 +226,12 @@ describe('getRecentAuditLogs', () => { test('defaults to limit 100', async () => { await getRecentAuditLogs() - expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 })) + expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ limit: 100 })) }) test('respects custom limit', async () => { await getRecentAuditLogs(25) - expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ take: 25 })) + expect(mockAuditLogFindMany).toHaveBeenCalledWith(expect.objectContaining({ limit: 25 })) }) }) diff --git a/src/lib/__tests__/portal-auth.test.ts b/src/lib/__tests__/portal-auth.test.ts index c12b2c65..2adb4c0d 100644 --- a/src/lib/__tests__/portal-auth.test.ts +++ b/src/lib/__tests__/portal-auth.test.ts @@ -7,6 +7,7 @@ */ import { getPortalAuth } from '../portal-auth' +import { eqParts, whereParts } from '@/test-utils/drizzle-where' // ============================================================================= // MOCKS @@ -19,11 +20,14 @@ jest.mock('next/headers', () => ({ cookies: () => mockCookies(), })) -const mockResidentFindUnique = jest.fn() +const mockResidentFindFirst = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - resident: { findUnique: (...args: unknown[]) => mockResidentFindUnique(...args) }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { findFirst: (...args: unknown[]) => mockResidentFindFirst(...args) }, + }, }, })) @@ -62,7 +66,7 @@ describe('getPortalAuth', () => { const result = await getPortalAuth() expect(result).toBeNull() - expect(mockResidentFindUnique).not.toHaveBeenCalled() + expect(mockResidentFindFirst).not.toHaveBeenCalled() }) test('returns null when resident_code cookie has no value', async () => { @@ -71,14 +75,14 @@ describe('getPortalAuth', () => { const result = await getPortalAuth() expect(result).toBeNull() - expect(mockResidentFindUnique).not.toHaveBeenCalled() + expect(mockResidentFindFirst).not.toHaveBeenCalled() }) // ── Resident not found ──────────────────────────────────────────────────── test('returns null when resident code does not match any resident', async () => { mockGet.mockReturnValue({ value: 'RES-INVALID' }) - mockResidentFindUnique.mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const result = await getPortalAuth() @@ -87,22 +91,19 @@ describe('getPortalAuth', () => { test('queries resident by exact code from cookie', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) await getPortalAuth() - expect(mockResidentFindUnique).toHaveBeenCalledWith( - expect.objectContaining({ - where: { code: RESIDENT_CODE }, - }), - ) + const { where } = mockResidentFindFirst.mock.calls[0][0] + expect(eqParts(where)).toEqual({ column: 'code', value: RESIDENT_CODE }) }) // ── Resident found but no active placement ──────────────────────────────── test('returns null when resident has no active placement', async () => { mockGet.mockReturnValue({ value: 'RES-002' }) - mockResidentFindUnique.mockResolvedValue(RESIDENT_WITHOUT_PLACEMENT) + mockResidentFindFirst.mockResolvedValue(RESIDENT_WITHOUT_PLACEMENT) const result = await getPortalAuth() @@ -113,7 +114,7 @@ describe('getPortalAuth', () => { test('returns resident and placement when authentication succeeds', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockResidentFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) const result = await getPortalAuth() @@ -126,7 +127,7 @@ describe('getPortalAuth', () => { test('uses the first active placement when multiple exist', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ ...RESIDENT_WITH_PLACEMENT, placements: [ { id: 'placement-id-1', housingUnitId: 'unit-id-1' }, @@ -143,51 +144,32 @@ describe('getPortalAuth', () => { test('only selects active placements in the query', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockResidentFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) await getPortalAuth() - expect(mockResidentFindUnique).toHaveBeenCalledWith( - expect.objectContaining({ - select: expect.objectContaining({ - placements: expect.objectContaining({ - where: { status: 'ACTIVE' }, - }), - }), - }), - ) + const call = mockResidentFindFirst.mock.calls[0][0] + expect(whereParts(call.with.placements.where)).toEqual({ status: 'ACTIVE' }) }) test('limits placement query to 1 record', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockResidentFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) await getPortalAuth() - expect(mockResidentFindUnique).toHaveBeenCalledWith( - expect.objectContaining({ - select: expect.objectContaining({ - placements: expect.objectContaining({ - take: 1, - }), - }), - }), - ) + const call = mockResidentFindFirst.mock.calls[0][0] + expect(call.with.placements.limit).toBe(1) }) test('selects minimal fields (id, code, placement id, housingUnitId)', async () => { mockGet.mockReturnValue({ value: RESIDENT_CODE }) - mockResidentFindUnique.mockResolvedValue(RESIDENT_WITH_PLACEMENT) + mockResidentFindFirst.mockResolvedValue(RESIDENT_WITH_PLACEMENT) await getPortalAuth() - const call = mockResidentFindUnique.mock.calls[0][0] - expect(call.select).toMatchObject({ - id: true, - code: true, - placements: expect.objectContaining({ - select: { id: true, housingUnitId: true }, - }), - }) + const call = mockResidentFindFirst.mock.calls[0][0] + expect(call.columns).toEqual({ id: true, code: true }) + expect(call.with.placements.columns).toEqual({ id: true, housingUnitId: true }) }) }) diff --git a/src/lib/__tests__/resident-ui-ssot.test.ts b/src/lib/__tests__/resident-ui-ssot.test.ts index a819f917..2ef6ea3c 100644 --- a/src/lib/__tests__/resident-ui-ssot.test.ts +++ b/src/lib/__tests__/resident-ui-ssot.test.ts @@ -18,7 +18,7 @@ */ import { readFileSync } from 'fs' import { join } from 'path' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' import { toResidentUiSummary } from '@/lib/housing/resident-ui' /** Fields that must never reach a client component (and thus the flight payload). */ diff --git a/src/lib/__tests__/scoring-ssot.test.ts b/src/lib/__tests__/scoring-ssot.test.ts index 528fa6c1..f3891ffb 100644 --- a/src/lib/__tests__/scoring-ssot.test.ts +++ b/src/lib/__tests__/scoring-ssot.test.ts @@ -1,7 +1,8 @@ /** * The compatibility algorithm has exactly ONE implementation. * - * `prisma/scoring-helper.ts` used to hold a second one, so the seed wrote + * The seed's scoring helper (now `scripts/db/scoring-helper.ts`) used to hold + * a second one, so the seed wrote * `Placement.compatibilityScore` values the product would never compute — a * whole database of plausible, wrong numbers that every demo, screenshot and * accuracy panel then reported as fact. Nothing caught it: it type-checked, @@ -18,7 +19,7 @@ import { readFileSync, readdirSync } from 'fs' import { join } from 'path' const REPO_ROOT = join(__dirname, '..', '..', '..') -const PRISMA_DIR = join(REPO_ROOT, 'prisma') +const SEED_DIR = join(REPO_ROOT, 'scripts', 'db') /** Weight tables, dimension math — the shapes a re-implementation takes. */ const SCORING_IMPLEMENTATION_MARKERS = [ @@ -27,21 +28,21 @@ const SCORING_IMPLEMENTATION_MARKERS = [ /lifestyle\s*:\s*\d+\s*,\s*\n?\s*social\s*:\s*\d+/, ] -function prismaTsFiles(): string[] { +function seedTsFiles(): string[] { const walk = (dir: string): string[] => readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { const full = join(dir, entry.name) - if (entry.isDirectory()) return entry.name === 'migrations' ? [] : walk(full) + if (entry.isDirectory()) return walk(full) return entry.name.endsWith('.ts') ? [full] : [] }) - return walk(PRISMA_DIR) + return walk(SEED_DIR) } describe('compatibility scoring SSOT', () => { it('no seed file re-implements the scoring math', () => { const offenders: string[] = [] - for (const file of prismaTsFiles()) { + for (const file of seedTsFiles()) { const source = readFileSync(file, 'utf8') for (const marker of SCORING_IMPLEMENTATION_MARKERS) { if (marker.test(source)) { @@ -54,7 +55,7 @@ describe('compatibility scoring SSOT', () => { }) it('the seed adapter delegates to the product algorithm', () => { - const source = readFileSync(join(PRISMA_DIR, 'scoring-helper.ts'), 'utf8') + const source = readFileSync(join(SEED_DIR, 'scoring-helper.ts'), 'utf8') expect(source).toContain('calculateCompatibility') expect(source).toContain('@/lib/compatibility') }) @@ -63,20 +64,20 @@ describe('compatibility scoring SSOT', () => { // Without this flag the seed cannot import the real algorithm at all, and // the next person hits exactly the wall that produced the duplicate. const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) - expect(pkg.prisma.seed).toContain('tsconfig-paths/register') + expect(pkg.scripts['db:seed']).toContain('tsconfig-paths/register') }) - it('every npm script running a prisma script registers the alias resolver', () => { + it('every npm script running a seed script registers the alias resolver', () => { const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) const offenders = Object.entries(pkg.scripts as Record) - .filter(([, cmd]) => /ts-node[^&|]*prisma\//.test(cmd)) + .filter(([, cmd]) => /ts-node[^&|]*scripts\/db\//.test(cmd)) .filter(([, cmd]) => !cmd.includes('tsconfig-paths/register')) .map(([name]) => name) expect(offenders).toEqual([]) }) - it('no workflow invokes ts-node on a prisma script by hand', () => { + it('no workflow invokes ts-node on a seed script by hand', () => { // CI seeded with a bare `npx ts-node prisma/seed.ts`, which is a SECOND // definition of "how to seed" — and the one that broke the moment the seed // began importing the product algorithm. Workflows call the npm scripts, @@ -87,7 +88,7 @@ describe('compatibility scoring SSOT', () => { for (const file of readdirSync(workflowDir).filter((f) => /\.ya?ml$/.test(f))) { const source = readFileSync(join(workflowDir, file), 'utf8') for (const line of source.split('\n')) { - if (!/ts-node[^&|]*prisma\//.test(line)) continue + if (!/ts-node[^&|]*scripts\/db\//.test(line)) continue if (line.trimStart().startsWith('#')) continue if (line.includes('tsconfig-paths/register')) continue offenders.push(`${file}: ${line.trim()}`) diff --git a/src/lib/actions/__tests__/appointment-requests.test.ts b/src/lib/actions/__tests__/appointment-requests.test.ts index 74156635..a85fbba8 100644 --- a/src/lib/actions/__tests__/appointment-requests.test.ts +++ b/src/lib/actions/__tests__/appointment-requests.test.ts @@ -10,20 +10,26 @@ * that cancels looks — to the person waiting — like being dropped. */ -import { prisma } from '@/lib/db' import { requestAppointment, respondToAppointmentRequest, rescheduleAppointment } from '../care' import { CARE_LABELS } from '@/lib/config/care' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' +const mockAppointmentFindFirst = jest.fn() +const mockCareAssignmentFindFirst = jest.fn() +const mockInsertValues = jest.fn() +const mockUpdateSet = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - appointment: { - create: jest.fn(), - findUnique: jest.fn(), - findFirst: jest.fn(), - update: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + appointment: { findFirst: (...a: unknown[]) => mockAppointmentFindFirst(...a) }, + careAssignment: { findFirst: (...a: unknown[]) => mockCareAssignmentFindFirst(...a) }, }, - careAssignment: { findUnique: jest.fn() }, + insert: jest.fn(() => ({ values: (v: unknown) => mockInsertValues(v) })), + update: jest.fn(() => ({ + set: (v: unknown) => ({ where: () => mockUpdateSet(v) }), + })), }, })) jest.mock('next/cache', () => ({ revalidatePath: jest.fn() })) @@ -39,11 +45,6 @@ jest.mock('@/lib/portal-auth', () => ({ getPortalAuth: (...args: unknown[]) => mockPortalAuth(...args), })) -const p = prisma as unknown as { - appointment: { create: jest.Mock; findUnique: jest.Mock; findFirst: jest.Mock; update: jest.Mock } - careAssignment: { findUnique: jest.Mock } -} - function form(entries: Record): FormData { const fd = new FormData() for (const [k, v] of Object.entries(entries)) fd.set(k, v) @@ -65,10 +66,10 @@ beforeEach(() => { jest.clearAllMocks() mockPortalAuth.mockResolvedValue({ resident: { id: 'res-1' } }) mockGetCurrentUser.mockResolvedValue({ id: 'staff-1', role: 'SOZIALARBEIT' }) - p.appointment.findFirst.mockResolvedValue(null) - p.careAssignment.findUnique.mockResolvedValue({ staffId: 'staff-9' }) - p.appointment.create.mockResolvedValue({ id: 'appt-1' }) - p.appointment.update.mockResolvedValue({}) + mockAppointmentFindFirst.mockResolvedValue(null) + mockCareAssignmentFindFirst.mockResolvedValue({ staffId: 'staff-9' }) + mockInsertValues.mockResolvedValue(undefined) + mockUpdateSet.mockResolvedValue(undefined) }) describe('a resident asks for a meeting', () => { @@ -78,23 +79,23 @@ describe('a resident asks for a meeting', () => { ) expect(result.success).toBe(true) - const [args] = p.appointment.create.mock.calls[0] - expect(args.data.status).toBe('REQUESTED') - expect(args.data.residentId).toBe('res-1') - expect(args.data.staffId).toBe('staff-9') - expect(args.data.residentNote).toBe('Brief vom Amt') + const [values] = mockInsertValues.mock.calls[0] + expect(values.status).toBe('REQUESTED') + expect(values.residentId).toBe('res-1') + expect(values.staffId).toBe('staff-9') + expect(values.residentNote).toBe('Brief vom Amt') }) it('still lands unclaimed when nobody holds the seat', async () => { // Zero real residents have a care team. Refusing here would have made the // feature dead on arrival for exactly the people who need it — the same // deadlock that killed caseload scoping. - p.careAssignment.findUnique.mockResolvedValue(null) + mockCareAssignmentFindFirst.mockResolvedValue(null) const result = await requestAppointment(form({ domain: 'JOB', startsAt: localInput(TOMORROW) })) expect(result.success).toBe(true) - expect(p.appointment.create.mock.calls[0][0].data.staffId).toBeNull() + expect(mockInsertValues.mock.calls[0][0].staffId).toBeNull() }) it('refuses a time in the past, which would be invisible the moment it saved', async () => { @@ -103,20 +104,20 @@ describe('a resident asks for a meeting', () => { ) expect(result).toEqual({ success: false, error: CARE_LABELS.requestPastTime }) - expect(p.appointment.create).not.toHaveBeenCalled() + expect(mockInsertValues).not.toHaveBeenCalled() }) it('refuses a second open request for the same seat', async () => { // A resident unsure the first one landed taps again, and a coach's queue // fills with duplicates of one ask. - p.appointment.findFirst.mockResolvedValue({ id: 'existing' }) + mockAppointmentFindFirst.mockResolvedValue({ id: 'existing' }) const result = await requestAppointment( form({ domain: 'SOCIAL', startsAt: localInput(TOMORROW) }), ) expect(result).toEqual({ success: false, error: CARE_LABELS.requestDuplicate }) - expect(p.appointment.create).not.toHaveBeenCalled() + expect(mockInsertValues).not.toHaveBeenCalled() }) it('refuses an unauthenticated caller', async () => { @@ -127,13 +128,13 @@ describe('a resident asks for a meeting', () => { ) expect(result.success).toBe(false) - expect(p.appointment.create).not.toHaveBeenCalled() + expect(mockInsertValues).not.toHaveBeenCalled() }) }) describe('staff answer the request', () => { beforeEach(() => { - p.appointment.findUnique.mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'SOCIAL', status: 'REQUESTED', @@ -143,16 +144,16 @@ describe('staff answer the request', () => { it('accepting schedules it and gives it an owner', async () => { await respondToAppointmentRequest(form({ id: 'a1', decision: 'ACCEPT' })) - const [args] = p.appointment.update.mock.calls[0] - expect(args.data.status).toBe('SCHEDULED') + const [values] = mockUpdateSet.mock.calls[0] + expect(values.status).toBe('SCHEDULED') // Whoever answered takes it — including on a request that arrived unclaimed. - expect(args.data.staffId).toBe('staff-1') + expect(values.staffId).toBe('staff-1') }) it('accepting without a new time keeps the time the resident proposed', async () => { await respondToAppointmentRequest(form({ id: 'a1', decision: 'ACCEPT' })) - expect(p.appointment.update.mock.calls[0][0].data.startsAt).toBeUndefined() + expect(mockUpdateSet.mock.calls[0][0].startsAt).toBeUndefined() }) it('declining REQUIRES a reason the resident will read', async () => { @@ -161,7 +162,7 @@ describe('staff answer the request', () => { ) expect(result).toEqual({ success: false, error: CARE_LABELS.declineNeedsReason }) - expect(p.appointment.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }) it('a decline carries its reason onto the record', async () => { @@ -169,9 +170,9 @@ describe('staff answer the request', () => { form({ id: 'a1', decision: 'DECLINE', staffNote: 'Diese Woche voll, nächste geht.' }), ) - const [args] = p.appointment.update.mock.calls[0] - expect(args.data.status).toBe('CANCELLED') - expect(args.data.staffNote).toBe('Diese Woche voll, nächste geht.') + const [values] = mockUpdateSet.mock.calls[0] + expect(values.status).toBe('CANCELLED') + expect(values.staffNote).toBe('Diese Woche voll, nächste geht.') }) it('refuses a staff member who does not work that seat', async () => { @@ -180,11 +181,11 @@ describe('staff answer the request', () => { const result = await respondToAppointmentRequest(form({ id: 'a1', decision: 'ACCEPT' })) expect(result).toEqual({ success: false, error: ERROR_MESSAGES.INSUFFICIENT_PERMISSIONS }) - expect(p.appointment.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }) it('refuses to answer something that is not an open request', async () => { - p.appointment.findUnique.mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'SOCIAL', status: 'COMPLETED', @@ -193,13 +194,13 @@ describe('staff answer the request', () => { const result = await respondToAppointmentRequest(form({ id: 'a1', decision: 'ACCEPT' })) expect(result.success).toBe(false) - expect(p.appointment.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }) }) describe('moving a meeting keeps the meeting', () => { it('changes the time in place rather than cancelling', async () => { - p.appointment.findUnique.mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'SOCIAL', status: 'SCHEDULED', @@ -209,18 +210,18 @@ describe('moving a meeting keeps the meeting', () => { form({ id: 'a1', startsAt: localInput(TOMORROW), staffNote: 'Eine Stunde später.' }), ) - const [args] = p.appointment.update.mock.calls[0] + const [values] = mockUpdateSet.mock.calls[0] // The status is untouched: to the resident this is a change, not a // cancellation followed by a different appointment appearing. - expect(args.data.status).toBeUndefined() - expect(args.data.startsAt).toBeInstanceOf(Date) - expect(args.data.staffNote).toBe('Eine Stunde später.') + expect(values.status).toBeUndefined() + expect(values.startsAt).toBeInstanceOf(Date) + expect(values.staffNote).toBe('Eine Stunde später.') }) it.each(['COMPLETED', 'CANCELLED', 'NO_SHOW'])( 'refuses to move a %s appointment, which is a record not a plan', async (status) => { - p.appointment.findUnique.mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'SOCIAL', status, @@ -229,12 +230,12 @@ describe('moving a meeting keeps the meeting', () => { const result = await rescheduleAppointment(form({ id: 'a1', startsAt: localInput(TOMORROW) })) expect(result).toEqual({ success: false, error: CARE_LABELS.rescheduleClosed }) - expect(p.appointment.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }, ) it('refuses a staff member who does not work that seat', async () => { - p.appointment.findUnique.mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'HOUSING', status: 'SCHEDULED', diff --git a/src/lib/actions/__tests__/care-appointment-checkin.test.ts b/src/lib/actions/__tests__/care-appointment-checkin.test.ts index 95825ecb..72a0e861 100644 --- a/src/lib/actions/__tests__/care-appointment-checkin.test.ts +++ b/src/lib/actions/__tests__/care-appointment-checkin.test.ts @@ -14,24 +14,40 @@ * 3. A resident with no active placement can still have appointments closed. */ -import { prisma } from '@/lib/db' +import { getTableName } from 'drizzle-orm' +import { appointment, placement } from '@/lib/db' import { setAppointmentStatus } from '../care' +const mockAppointmentFindFirst = jest.fn() +const mockPlacementFindFirst = jest.fn() +const mockCheckInCreate = jest.fn() +/** Records every update — direct or in a transaction — as (tableName, payload). */ +const mockUpdateSet = jest.fn() + jest.mock('@/lib/db', () => { - // Annotated because $transaction refers to prismaMock inside its own - // initializer, which otherwise infers as `any` under strict mode. - const prismaMock: { - appointment: { findUnique: jest.Mock; update: jest.Mock } - placement: { findFirst: jest.Mock; update: jest.Mock } - satisfactionCheckIn: { create: jest.Mock } - $transaction: jest.Mock - } = { - appointment: { findUnique: jest.fn(), update: jest.fn() }, - placement: { findFirst: jest.fn(), update: jest.fn() }, - satisfactionCheckIn: { create: jest.fn() }, - $transaction: jest.fn(async (cb: (tx: unknown) => Promise) => cb(prismaMock)), + const update = jest.fn((table: unknown) => ({ + set: (v: unknown) => { + const { getTableName: tableName } = require('drizzle-orm') + + mockUpdateSet(tableName(table as any), v) + return { where: () => Promise.resolve([]) } + }, + })) + const tx = { + insert: jest.fn(() => ({ values: (v: unknown) => mockCheckInCreate(v) })), + update, + } + return { + ...jest.requireActual('@/lib/db'), + db: { + query: { + appointment: { findFirst: (...a: unknown[]) => mockAppointmentFindFirst(...a) }, + placement: { findFirst: (...a: unknown[]) => mockPlacementFindFirst(...a) }, + }, + update, + transaction: async (cb: (t: unknown) => Promise) => cb(tx), + }, } - return { prisma: prismaMock } }) jest.mock('next/cache', () => ({ revalidatePath: jest.fn() })) @@ -48,8 +64,6 @@ jest.mock('@/lib/auth', () => ({ })), })) -const mockPrisma = prisma as jest.Mocked - function completionForm(fields: Record = {}): FormData { const fd = new FormData() fd.set('id', 'appt-1') @@ -58,20 +72,23 @@ function completionForm(fields: Record = {}): FormData { return fd } +/** The one update setAppointmentStatus always makes: the status itself. */ +function appointmentStatusUpdates() { + return mockUpdateSet.mock.calls.filter(([table]) => table === getTableName(appointment)) +} + beforeEach(() => { jest.clearAllMocks() - ;(mockPrisma.appointment.findUnique as jest.Mock).mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'HOUSING', checkIn: null, }) - ;(mockPrisma.appointment.update as jest.Mock).mockResolvedValue({}) - ;(mockPrisma.placement.findFirst as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ id: 'pl-1', startDate: new Date('2026-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ id: 'ci-1' }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockCheckInCreate.mockResolvedValue({ id: 'ci-1' }) }) describe('completing an appointment', () => { @@ -79,8 +96,8 @@ describe('completing an appointment', () => { const result = await setAppointmentStatus(completionForm()) expect(result).toEqual({ success: true }) - expect(mockPrisma.appointment.update).toHaveBeenCalled() - expect(mockPrisma.satisfactionCheckIn.create).not.toHaveBeenCalled() + expect(appointmentStatusUpdates()).not.toHaveLength(0) + expect(mockCheckInCreate).not.toHaveBeenCalled() }) it.each(['0', '6', '', 'not a number', '3.5'])( @@ -88,7 +105,7 @@ describe('completing an appointment', () => { async (value) => { await setAppointmentStatus(completionForm({ overallSatisfaction: value })) - expect(mockPrisma.satisfactionCheckIn.create).not.toHaveBeenCalled() + expect(mockCheckInCreate).not.toHaveBeenCalled() }, ) @@ -97,33 +114,32 @@ describe('completing an appointment', () => { completionForm({ overallSatisfaction: '4', concerns: 'Lärm nachts' }), ) - expect(mockPrisma.satisfactionCheckIn.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockCheckInCreate).toHaveBeenCalledWith( + expect.objectContaining({ placementId: 'pl-1', appointmentId: 'appt-1', overallSatisfaction: 4, concerns: 'Lärm nachts', collectedByUserId: staff.id, }), - }) - expect(mockPrisma.placement.update).toHaveBeenCalledWith({ - where: { id: 'pl-1' }, - data: { satisfactionRating: 4 }, + ) + expect(mockUpdateSet).toHaveBeenCalledWith(getTableName(placement), { + satisfactionRating: 4, }) }) it('still closes the appointment when the resident has no active placement', async () => { - ;(mockPrisma.placement.findFirst as jest.Mock).mockResolvedValue(null) + mockPlacementFindFirst.mockResolvedValue(null) const result = await setAppointmentStatus(completionForm({ overallSatisfaction: '5' })) expect(result).toEqual({ success: true }) - expect(mockPrisma.appointment.update).toHaveBeenCalled() - expect(mockPrisma.satisfactionCheckIn.create).not.toHaveBeenCalled() + expect(appointmentStatusUpdates()).not.toHaveLength(0) + expect(mockCheckInCreate).not.toHaveBeenCalled() }) it('does not overwrite a reading already recorded for this appointment', async () => { - ;(mockPrisma.appointment.findUnique as jest.Mock).mockResolvedValue({ + mockAppointmentFindFirst.mockResolvedValue({ residentId: 'res-1', domain: 'HOUSING', checkIn: { id: 'ci-existing' }, @@ -131,7 +147,7 @@ describe('completing an appointment', () => { await setAppointmentStatus(completionForm({ overallSatisfaction: '2' })) - expect(mockPrisma.satisfactionCheckIn.create).not.toHaveBeenCalled() + expect(mockCheckInCreate).not.toHaveBeenCalled() }) it('records nothing when the appointment is cancelled rather than held', async () => { @@ -140,6 +156,6 @@ describe('completing an appointment', () => { await setAppointmentStatus(fd) - expect(mockPrisma.satisfactionCheckIn.create).not.toHaveBeenCalled() + expect(mockCheckInCreate).not.toHaveBeenCalled() }) }) diff --git a/src/lib/actions/__tests__/config.test.ts b/src/lib/actions/__tests__/config.test.ts index 35ff155a..7bf39a0c 100644 --- a/src/lib/actions/__tests__/config.test.ts +++ b/src/lib/actions/__tests__/config.test.ts @@ -5,19 +5,29 @@ * saveSystemConfig uses a custom parseFloat that treats empty/negative/NaN as null. */ -import { prisma } from '@/lib/db' +import { eq } from 'drizzle-orm' +import { systemConfig } from '@/lib/db' import { getSystemConfig, saveSystemConfig } from '../config' // ============================================================================= // MOCKS // ============================================================================= +const mockConfigFindFirst = jest.fn() +// Receives (valuesPayload, onConflictConfig) — the drizzle equivalent of upsert +const mockConfigUpsert = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - systemConfig: { - findUnique: jest.fn(), - upsert: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + systemConfig: { findFirst: (...a: unknown[]) => mockConfigFindFirst(...a) }, }, + insert: jest.fn(() => ({ + values: (v: unknown) => ({ + onConflictDoUpdate: (cfg: unknown): Promise => mockConfigUpsert(v, cfg), + }), + })), }, })) @@ -42,8 +52,6 @@ jest.mock('@/lib/auth', () => ({ }), })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -66,7 +74,7 @@ function makeConfigFormData(overrides: Record = {}): FormData { describe('getSystemConfig', () => { it('returns all nulls when no config row exists', async () => { - ;(mockPrisma.systemConfig.findUnique as jest.Mock).mockResolvedValue(null) + mockConfigFindFirst.mockResolvedValue(null) const result = await getSystemConfig() @@ -80,7 +88,7 @@ describe('getSystemConfig', () => { it('returns stored values when config exists', async () => { const startDate = new Date('2024-03-01') - ;(mockPrisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({ + mockConfigFindFirst.mockResolvedValue({ id: 'singleton', pilotBaselineIncidentsPerMonth: 15, pilotBaselineRelocationsPerMonth: 4, @@ -99,7 +107,7 @@ describe('getSystemConfig', () => { }) it('returns nulls for missing optional fields in existing row', async () => { - ;(mockPrisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({ + mockConfigFindFirst.mockResolvedValue({ id: 'singleton', pilotBaselineIncidentsPerMonth: null, pilotBaselineRelocationsPerMonth: null, @@ -114,12 +122,12 @@ describe('getSystemConfig', () => { }) it('queries by singleton id', async () => { - ;(mockPrisma.systemConfig.findUnique as jest.Mock).mockResolvedValue(null) + mockConfigFindFirst.mockResolvedValue(null) await getSystemConfig() - expect(mockPrisma.systemConfig.findUnique).toHaveBeenCalledWith({ - where: { id: 'singleton' }, + expect(mockConfigFindFirst).toHaveBeenCalledWith({ + where: eq(systemConfig.id, 'singleton'), }) }) }) @@ -130,7 +138,7 @@ describe('getSystemConfig', () => { describe('saveSystemConfig', () => { it('saves valid numeric values', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotBaselineIncidentsPerMonth: '15', @@ -140,14 +148,14 @@ describe('saveSystemConfig', () => { await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineIncidentsPerMonth: 15, - pilotBaselineRelocationsPerMonth: 4, - pilotBaselineMediationHoursPerWeek: 12, - }), - update: expect.objectContaining({ + pilotBaselineIncidentsPerMonth: 15, + pilotBaselineRelocationsPerMonth: 4, + pilotBaselineMediationHoursPerWeek: 12, + }), + expect.objectContaining({ + set: expect.objectContaining({ pilotBaselineIncidentsPerMonth: 15, pilotBaselineRelocationsPerMonth: 4, pilotBaselineMediationHoursPerWeek: 12, @@ -157,24 +165,23 @@ describe('saveSystemConfig', () => { }) it('treats empty fields as null', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) await saveSystemConfig(new FormData()) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineIncidentsPerMonth: null, - pilotBaselineRelocationsPerMonth: null, - pilotBaselineMediationHoursPerWeek: null, - pilotStartDate: null, - }), + pilotBaselineIncidentsPerMonth: null, + pilotBaselineRelocationsPerMonth: null, + pilotBaselineMediationHoursPerWeek: null, + pilotStartDate: null, }), + expect.anything(), ) }) it('treats negative values as null', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotBaselineIncidentsPerMonth: '-5', @@ -183,18 +190,17 @@ describe('saveSystemConfig', () => { await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineIncidentsPerMonth: null, - pilotBaselineRelocationsPerMonth: null, - }), + pilotBaselineIncidentsPerMonth: null, + pilotBaselineRelocationsPerMonth: null, }), + expect.anything(), ) }) it('treats non-numeric strings as null', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotBaselineIncidentsPerMonth: 'abc', @@ -203,18 +209,17 @@ describe('saveSystemConfig', () => { await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineIncidentsPerMonth: null, - pilotBaselineMediationHoursPerWeek: null, - }), + pilotBaselineIncidentsPerMonth: null, + pilotBaselineMediationHoursPerWeek: null, }), + expect.anything(), ) }) it('accepts zero as a valid value', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotBaselineIncidentsPerMonth: '0', @@ -222,17 +227,16 @@ describe('saveSystemConfig', () => { await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineIncidentsPerMonth: 0, - }), + pilotBaselineIncidentsPerMonth: 0, }), + expect.anything(), ) }) it('accepts decimal values', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotBaselineMediationHoursPerWeek: '7.5', @@ -240,43 +244,42 @@ describe('saveSystemConfig', () => { await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotBaselineMediationHoursPerWeek: 7.5, - }), + pilotBaselineMediationHoursPerWeek: 7.5, }), + expect.anything(), ) }) it('saves a valid pilot start date', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const fd = makeConfigFormData({ pilotStartDate: '2024-03-01' }) await saveSystemConfig(fd) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( + expect(mockConfigUpsert).toHaveBeenCalledWith( expect.objectContaining({ - create: expect.objectContaining({ - pilotStartDate: new Date('2024-03-01'), - }), + pilotStartDate: new Date('2024-03-01'), }), + expect.anything(), ) }) it('uses singleton upsert key', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) await saveSystemConfig(new FormData()) - expect(mockPrisma.systemConfig.upsert).toHaveBeenCalledWith( - expect.objectContaining({ where: { id: 'singleton' } }), + expect(mockConfigUpsert).toHaveBeenCalledWith( + expect.objectContaining({ id: 'singleton' }), + expect.objectContaining({ target: systemConfig.id }), ) }) it('revalidates settings and analytics paths', async () => { - ;(mockPrisma.systemConfig.upsert as jest.Mock).mockResolvedValue({}) + mockConfigUpsert.mockResolvedValue({}) const { revalidatePath } = require('next/cache') await saveSystemConfig(new FormData()) @@ -290,6 +293,6 @@ describe('saveSystemConfig', () => { requirePermission.mockRejectedValueOnce(new Error('Anmeldung erforderlich')) await expect(saveSystemConfig(new FormData())).rejects.toThrow('Anmeldung erforderlich') - expect(mockPrisma.systemConfig.upsert).not.toHaveBeenCalled() + expect(mockConfigUpsert).not.toHaveBeenCalled() }) }) diff --git a/src/lib/actions/__tests__/housing.test.ts b/src/lib/actions/__tests__/housing.test.ts index 9afea15b..8b5937cb 100644 --- a/src/lib/actions/__tests__/housing.test.ts +++ b/src/lib/actions/__tests__/housing.test.ts @@ -5,7 +5,8 @@ * createHousingUnit/updateHousingUnit use redirect() which throws, so they are not tested here. */ -import { prisma } from '@/lib/db' +import { getTableName } from 'drizzle-orm' +import { placement, incident, maintenanceRequest, placementSpot, householdTask } from '@/lib/db' import { logAudit } from '@/lib/audit' import { archiveHousingUnit, restoreHousingUnit, hardDeleteHousingUnitProtected } from '../housing' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -14,20 +15,30 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockHousingUnitFindFirst = jest.fn() +const mockUpdateSet = jest.fn() +const mockDelete = jest.fn() +const mockCount = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - housingUnit: { - findUnique: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + housingUnit: { findFirst: (...a: unknown[]) => mockHousingUnitFindFirst(...a) }, }, - placement: { count: jest.fn() }, - incident: { count: jest.fn() }, - maintenanceRequest: { count: jest.fn() }, - placementSpot: { count: jest.fn() }, - householdTask: { count: jest.fn() }, - auditLog: { create: jest.fn() }, + update: jest.fn(() => ({ + set: (v: unknown) => { + mockUpdateSet(v) + return { + where: () => + Object.assign(Promise.resolve([]), { + returning: (): Promise => Promise.resolve([{ id: 'hu-1' }]), + }), + } + }, + })), + delete: jest.fn(() => ({ where: (w: unknown) => mockDelete(w) })), + $count: (table: unknown, where: unknown) => mockCount(table, where), }, })) @@ -81,10 +92,16 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked +/** Route db.$count(table, where) by table, like the old per-model count mocks. */ +function mockCountsByTable(counts: Record) { + mockCount.mockImplementation((table: unknown) => + Promise.resolve(counts[getTableName(table as any)] ?? 0), + ) +} beforeEach(() => { jest.clearAllMocks() + mockDelete.mockResolvedValue(undefined) }) // ============================================================================= @@ -93,16 +110,16 @@ beforeEach(() => { describe('archiveHousingUnit', () => { it('returns error when housing unit not found', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue(null) + mockHousingUnitFindFirst.mockResolvedValue(null) const result = await archiveHousingUnit('nonexistent-id') expect(result).toEqual({ success: false, error: ERROR_MESSAGES.UNIT_NOT_FOUND }) - expect(mockPrisma.housingUnit.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }) it('returns error when unit has active placements', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'WG-001', placements: [{ id: 'pl-1' }], @@ -113,11 +130,11 @@ describe('archiveHousingUnit', () => { expect(result.success).toBe(false) expect(result.error).toContain('aktive Belegung') - expect(mockPrisma.housingUnit.update).not.toHaveBeenCalled() + expect(mockUpdateSet).not.toHaveBeenCalled() }) it('returns error when unit has occupied spots', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'WG-001', placements: [], @@ -131,21 +148,17 @@ describe('archiveHousingUnit', () => { }) it('succeeds and sets status to CLOSED when no active occupancy', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'WG-001', placements: [], spots: [], }) - ;(mockPrisma.housingUnit.update as jest.Mock).mockResolvedValue({ id: 'hu-1' }) const result = await archiveHousingUnit('hu-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.housingUnit.update).toHaveBeenCalledWith({ - where: { id: 'hu-1' }, - data: { status: 'CLOSED' }, - }) + expect(mockUpdateSet).toHaveBeenCalledWith({ status: 'CLOSED' }) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'ARCHIVE', @@ -163,7 +176,7 @@ describe('archiveHousingUnit', () => { describe('restoreHousingUnit', () => { it('returns error when housing unit not found', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue(null) + mockHousingUnitFindFirst.mockResolvedValue(null) const result = await restoreHousingUnit('nonexistent-id') @@ -171,20 +184,16 @@ describe('restoreHousingUnit', () => { }) it('succeeds and sets status to AVAILABLE', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'WG-001', status: 'CLOSED', }) - ;(mockPrisma.housingUnit.update as jest.Mock).mockResolvedValue({ id: 'hu-1' }) const result = await restoreHousingUnit('hu-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.housingUnit.update).toHaveBeenCalledWith({ - where: { id: 'hu-1' }, - data: { status: 'AVAILABLE' }, - }) + expect(mockUpdateSet).toHaveBeenCalledWith({ status: 'AVAILABLE' }) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'RESTORE', @@ -209,7 +218,7 @@ describe('hardDeleteHousingUnitProtected', () => { ) expect(result).toEqual({ success: false, error: 'Bestätigung fehlt (DELETE)' }) - expect(mockPrisma.housingUnit.findUnique).not.toHaveBeenCalled() + expect(mockHousingUnitFindFirst).not.toHaveBeenCalled() }) it('returns error when reason is too short', async () => { @@ -220,7 +229,7 @@ describe('hardDeleteHousingUnitProtected', () => { }) it('returns error when housing unit not found', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue(null) + mockHousingUnitFindFirst.mockResolvedValue(null) const result = await hardDeleteHousingUnitProtected('hu-1', 'DELETE', 'Testdaten bereinigen') @@ -228,7 +237,7 @@ describe('hardDeleteHousingUnitProtected', () => { }) it('returns error when housing unit is not test/demo', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'WG-001', }) @@ -240,15 +249,17 @@ describe('hardDeleteHousingUnitProtected', () => { }) it('returns error with blocker report when unit has linked history', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'test-wg-1', }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(3) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(1) - ;(mockPrisma.maintenanceRequest.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placementSpot.count as jest.Mock).mockResolvedValue(4) - ;(mockPrisma.householdTask.count as jest.Mock).mockResolvedValue(0) + mockCountsByTable({ + [getTableName(placement)]: 3, + [getTableName(incident)]: 1, + [getTableName(maintenanceRequest)]: 0, + [getTableName(placementSpot)]: 4, + [getTableName(householdTask)]: 0, + }) const result = await hardDeleteHousingUnitProtected('hu-1', 'DELETE', 'Testdaten bereinigen') @@ -261,21 +272,16 @@ describe('hardDeleteHousingUnitProtected', () => { }) it('succeeds for test housing unit with no linked history', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue({ + mockHousingUnitFindFirst.mockResolvedValue({ id: 'hu-1', code: 'demo-wg-1', }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.maintenanceRequest.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placementSpot.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.householdTask.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.housingUnit.delete as jest.Mock).mockResolvedValue({ id: 'hu-1' }) + mockCountsByTable({}) const result = await hardDeleteHousingUnitProtected('hu-1', 'DELETE', 'Testdaten bereinigen') expect(result).toEqual({ success: true }) - expect(mockPrisma.housingUnit.delete).toHaveBeenCalledWith({ where: { id: 'hu-1' } }) + expect(mockDelete).toHaveBeenCalledTimes(1) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'DELETE', diff --git a/src/lib/actions/__tests__/incidents.test.ts b/src/lib/actions/__tests__/incidents.test.ts index a4aa5888..ed8678b8 100644 --- a/src/lib/actions/__tests__/incidents.test.ts +++ b/src/lib/actions/__tests__/incidents.test.ts @@ -6,8 +6,6 @@ * createIncident/addFollowUp use redirect() or are form-data-dependent. */ -import { prisma } from '@/lib/db' -import { logAudit } from '@/lib/audit' import { getResidentIncidentStats, getHousingUnitIncidentHistory, @@ -19,22 +17,33 @@ import { // MOCKS // ============================================================================= +// db.$count(incident, …) is called for "reported" then "as subject", then +// db.$count(incidentInvolvement, …) for "involved" — order is deterministic +// (Promise.all array), so Once-chains keep the old per-model discrimination. +const mockCount = jest.fn() +const mockIncidentFindMany = jest.fn() +const mockIncidentFindFirst = jest.fn() +const mockUpdateSet = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - incident: { - findUnique: jest.fn(), - findMany: jest.fn(), - update: jest.fn(), - create: jest.fn(), - count: jest.fn(), - }, - incidentInvolvement: { - count: jest.fn(), - }, - incidentFollowUp: { - create: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + incident: { + findMany: (...a: unknown[]) => mockIncidentFindMany(...a), + findFirst: (...a: unknown[]) => mockIncidentFindFirst(...a), + }, }, - auditLog: { create: jest.fn() }, + $count: (table: unknown, where: unknown) => mockCount(table, where), + // Subquery builder used inside inArray(); never executed because + // findMany is mocked — it only needs to be chainable. + select: jest.fn(() => ({ from: jest.fn(() => ({ where: jest.fn(() => ({})) })) })), + update: jest.fn(() => ({ + set: (v: unknown) => { + mockUpdateSet(v) + return { where: () => Promise.resolve([]) } + }, + })), }, })) @@ -88,8 +97,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -100,11 +107,11 @@ beforeEach(() => { describe('getResidentIncidentStats', () => { it('returns zero counts when resident has no incidents', async () => { - ;(mockPrisma.incident.count as jest.Mock) + mockCount .mockResolvedValueOnce(0) // reported .mockResolvedValueOnce(0) // as subject - ;(mockPrisma.incidentInvolvement.count as jest.Mock).mockResolvedValue(0) // involved - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValue([]) // unique incidents + .mockResolvedValueOnce(0) // involved + mockIncidentFindMany.mockResolvedValue([]) // unique incidents const stats = await getResidentIncidentStats('res-1') @@ -117,11 +124,11 @@ describe('getResidentIncidentStats', () => { }) it('returns correct counts for a resident with mixed incident involvement', async () => { - ;(mockPrisma.incident.count as jest.Mock) + mockCount .mockResolvedValueOnce(3) // reported .mockResolvedValueOnce(1) // as subject - ;(mockPrisma.incidentInvolvement.count as jest.Mock).mockResolvedValue(2) // involved - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValue([ + .mockResolvedValueOnce(2) // involved + mockIncidentFindMany.mockResolvedValue([ { id: 'inc-1' }, { id: 'inc-2' }, { id: 'inc-3' }, @@ -145,7 +152,7 @@ describe('getResidentIncidentStats', () => { describe('getHousingUnitIncidentHistory', () => { it('returns empty data for a unit with no incidents', async () => { - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValue([]) + mockIncidentFindMany.mockResolvedValue([]) const result = await getHousingUnitIncidentHistory('hu-1') @@ -186,7 +193,7 @@ describe('getHousingUnitIncidentHistory', () => { involvedResidents: [], }, ] - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValue(incidents) + mockIncidentFindMany.mockResolvedValue(incidents) const result = await getHousingUnitIncidentHistory('hu-1') @@ -221,7 +228,7 @@ describe('getHousingUnitIncidentHistory', () => { involvedResidents: [], }, ] - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValue(incidents) + mockIncidentFindMany.mockResolvedValue(incidents) const result = await getHousingUnitIncidentHistory('hu-1') @@ -235,11 +242,17 @@ describe('getHousingUnitIncidentHistory', () => { describe('getIncidentsNeedingFollowUp', () => { it('returns categorized incidents for follow-up', async () => { - const overdueIncident = { id: 'inc-overdue', nextFollowUpDate: new Date('2025-01-01') } - const dueSoonIncident = { id: 'inc-soon', nextFollowUpDate: new Date() } - const urgentIncident = { id: 'inc-urgent', followUpPriority: 'URGENT' } - - ;(mockPrisma.incident.findMany as jest.Mock) + // followUps rows are fetched (and counted in memory) where Prisma used + // a `_count` include, so the fixtures carry the (empty) relation. + const overdueIncident = { + id: 'inc-overdue', + nextFollowUpDate: new Date('2025-01-01'), + followUps: [], + } + const dueSoonIncident = { id: 'inc-soon', nextFollowUpDate: new Date(), followUps: [] } + const urgentIncident = { id: 'inc-urgent', followUpPriority: 'URGENT', followUps: [] } + + mockIncidentFindMany .mockResolvedValueOnce([overdueIncident]) // overdue .mockResolvedValueOnce([dueSoonIncident]) // dueSoon .mockResolvedValueOnce([urgentIncident]) // urgent @@ -252,7 +265,7 @@ describe('getIncidentsNeedingFollowUp', () => { }) it('returns empty arrays when no follow-ups needed', async () => { - ;(mockPrisma.incident.findMany as jest.Mock) + mockIncidentFindMany .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) diff --git a/src/lib/actions/__tests__/maintenance.test.ts b/src/lib/actions/__tests__/maintenance.test.ts index d5ee8da5..3360ec7b 100644 --- a/src/lib/actions/__tests__/maintenance.test.ts +++ b/src/lib/actions/__tests__/maintenance.test.ts @@ -7,7 +7,8 @@ * createMaintenanceRequest uses redirect() which throws, so we mock it to throw NEXT_REDIRECT. */ -import { prisma } from '@/lib/db' +import { maintenanceRequest } from '@/lib/db' +import { desc, eq } from 'drizzle-orm' import { logAudit } from '@/lib/audit' import { createMaintenanceRequest, @@ -22,13 +23,29 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockInsertReturning = jest.fn() +const mockUpdateReturning = jest.fn() +const mockUpdateWhere = jest.fn() +const mockCount = jest.fn() +const mockFindMany = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - maintenanceRequest: { - create: jest.fn(), - update: jest.fn(), - count: jest.fn(), - findMany: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + insert: jest.fn(() => ({ + values: (v: unknown) => ({ returning: (): Promise => mockInsertReturning(v) }), + })), + update: jest.fn(() => ({ + set: (v: unknown) => ({ + where: (w: unknown) => { + mockUpdateWhere(w) + return { returning: (): Promise => mockUpdateReturning(v) } + }, + }), + })), + $count: (...a: unknown[]) => mockCount(...a), + query: { + maintenanceRequest: { findMany: (...a: unknown[]) => mockFindMany(...a) }, }, }, })) @@ -87,8 +104,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -178,15 +193,17 @@ describe('createMaintenanceRequest', () => { }) it('creates maintenance request and redirects on success', async () => { - ;(mockPrisma.maintenanceRequest.create as jest.Mock).mockResolvedValue({ - id: 'mr-1', - housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', - }) + mockInsertReturning.mockResolvedValue([ + { + id: 'mr-1', + housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', + }, + ]) await expect(createMaintenanceRequest(makeCreateFormData())).rejects.toThrow('NEXT_REDIRECT') - expect(mockPrisma.maintenanceRequest.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', category: 'PLUMBING', priority: 'NORMAL', @@ -194,7 +211,7 @@ describe('createMaintenanceRequest', () => { description: 'Kitchen faucet is leaking', status: 'OPEN', }), - }) + ) expect(logAudit).toHaveBeenCalledWith({ action: 'CREATE', @@ -212,10 +229,12 @@ describe('createMaintenanceRequest', () => { }) it('creates request with optional fields when provided', async () => { - ;(mockPrisma.maintenanceRequest.create as jest.Mock).mockResolvedValue({ - id: 'mr-2', - housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', - }) + mockInsertReturning.mockResolvedValue([ + { + id: 'mr-2', + housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', + }, + ]) const fd = makeCreateFormData({ spotId: 'clxxxxxxxxxxxxxxxxx0020', @@ -226,18 +245,18 @@ describe('createMaintenanceRequest', () => { await expect(createMaintenanceRequest(fd)).rejects.toThrow('NEXT_REDIRECT') - expect(mockPrisma.maintenanceRequest.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockInsertReturning).toHaveBeenCalledWith( + expect.objectContaining({ spotId: 'clxxxxxxxxxxxxxxxxx0020', location: 'Kitchen', reporterName: 'Maria', reportedById: 'clxxxxxxxxxxxxxxxxx0030', }), - }) + ) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.maintenanceRequest.create as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the insert fails', async () => { + mockInsertReturning.mockRejectedValue(new Error('DB error')) await expect(createMaintenanceRequest(makeCreateFormData())).rejects.toThrow( ERROR_MESSAGES.MAINTENANCE_CREATE_ERROR, @@ -257,20 +276,19 @@ describe('updateMaintenanceStatus', () => { }) it('updates status to IN_PROGRESS', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockResolvedValue({ - housingUnitId: 'hu-1', - }) + mockUpdateReturning.mockResolvedValue([{ housingUnitId: 'hu-1' }]) await updateMaintenanceStatus(makeStatusUpdateFormData()) - expect(mockPrisma.maintenanceRequest.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: expect.objectContaining({ + expect(mockUpdateWhere).toHaveBeenCalledWith( + eq(maintenanceRequest.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ status: 'IN_PROGRESS', startedAt: expect.any(Date), }), - select: { housingUnitId: true }, - }) + ) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ @@ -283,9 +301,7 @@ describe('updateMaintenanceStatus', () => { }) it('updates status to COMPLETED with resolution and cost', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockResolvedValue({ - housingUnitId: 'hu-1', - }) + mockUpdateReturning.mockResolvedValue([{ housingUnitId: 'hu-1' }]) const fd = makeStatusUpdateFormData({ status: 'COMPLETED', @@ -295,22 +311,21 @@ describe('updateMaintenanceStatus', () => { await updateMaintenanceStatus(fd) - expect(mockPrisma.maintenanceRequest.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: expect.objectContaining({ + expect(mockUpdateWhere).toHaveBeenCalledWith( + eq(maintenanceRequest.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ status: 'COMPLETED', completedAt: expect.any(Date), resolution: 'Fixed the pipe', cost: 150, }), - select: { housingUnitId: true }, - }) + ) }) it('updates status to ASSIGNED with assignedTo', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockResolvedValue({ - housingUnitId: 'hu-1', - }) + mockUpdateReturning.mockResolvedValue([{ housingUnitId: 'hu-1' }]) const fd = makeStatusUpdateFormData({ status: 'ASSIGNED', @@ -319,21 +334,20 @@ describe('updateMaintenanceStatus', () => { await updateMaintenanceStatus(fd) - expect(mockPrisma.maintenanceRequest.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: expect.objectContaining({ + expect(mockUpdateWhere).toHaveBeenCalledWith( + eq(maintenanceRequest.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ status: 'ASSIGNED', assignedTo: 'Hans Mueller', assignedAt: expect.any(Date), }), - select: { housingUnitId: true }, - }) + ) }) it('includes notes when provided', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockResolvedValue({ - housingUnitId: 'hu-1', - }) + mockUpdateReturning.mockResolvedValue([{ housingUnitId: 'hu-1' }]) const fd = makeStatusUpdateFormData({ notes: 'Waiting for parts', @@ -342,18 +356,19 @@ describe('updateMaintenanceStatus', () => { await updateMaintenanceStatus(fd) - expect(mockPrisma.maintenanceRequest.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: expect.objectContaining({ + expect(mockUpdateWhere).toHaveBeenCalledWith( + eq(maintenanceRequest.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ status: 'ON_HOLD', notes: 'Waiting for parts', }), - select: { housingUnitId: true }, - }) + ) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the update fails', async () => { + mockUpdateReturning.mockRejectedValue(new Error('DB error')) await expect(updateMaintenanceStatus(makeStatusUpdateFormData())).rejects.toThrow( ERROR_MESSAGES.MAINTENANCE_STATUS_UPDATE_ERROR, @@ -373,20 +388,17 @@ describe('assignMaintenanceRequest', () => { }) it('assigns request and sets status to ASSIGNED', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockResolvedValue({ - housingUnitId: 'hu-1', - }) + mockUpdateReturning.mockResolvedValue([{ housingUnitId: 'hu-1' }]) await assignMaintenanceRequest(makeAssignFormData()) - expect(mockPrisma.maintenanceRequest.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: { - status: 'ASSIGNED', - assignedTo: 'Hans Mueller', - assignedAt: expect.any(Date), - }, - select: { housingUnitId: true }, + expect(mockUpdateWhere).toHaveBeenCalledWith( + eq(maintenanceRequest.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) + expect(mockUpdateReturning).toHaveBeenCalledWith({ + status: 'ASSIGNED', + assignedTo: 'Hans Mueller', + assignedAt: expect.any(Date), }) expect(logAudit).toHaveBeenCalledWith({ @@ -398,8 +410,8 @@ describe('assignMaintenanceRequest', () => { }) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.maintenanceRequest.update as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the update fails', async () => { + mockUpdateReturning.mockRejectedValue(new Error('DB error')) await expect(assignMaintenanceRequest(makeAssignFormData())).rejects.toThrow( ERROR_MESSAGES.MAINTENANCE_ASSIGN_ERROR, @@ -412,9 +424,9 @@ describe('assignMaintenanceRequest', () => { // ============================================================================= describe('getMaintenanceStats', () => { - it('returns correct stats from prisma counts', async () => { + it('returns correct stats from db counts', async () => { // Mock the Promise.all counts: open, assigned, inProgress, onHold, completedThisMonth - ;(mockPrisma.maintenanceRequest.count as jest.Mock) + mockCount .mockResolvedValueOnce(5) // open .mockResolvedValueOnce(3) // assigned .mockResolvedValueOnce(2) // inProgress @@ -436,7 +448,7 @@ describe('getMaintenanceStats', () => { }) it('returns zeros when no requests exist', async () => { - ;(mockPrisma.maintenanceRequest.count as jest.Mock).mockResolvedValue(0) + mockCount.mockResolvedValue(0) const stats = await getMaintenanceStats() @@ -462,24 +474,24 @@ describe('getHousingUnitMaintenance', () => { { id: 'mr-1', title: 'Leaky faucet', status: 'OPEN' }, { id: 'mr-2', title: 'Broken window', status: 'COMPLETED' }, ] - ;(mockPrisma.maintenanceRequest.findMany as jest.Mock).mockResolvedValue(mockRequests) + mockFindMany.mockResolvedValue(mockRequests) const result = await getHousingUnitMaintenance('hu-1') expect(result).toEqual(mockRequests) - expect(mockPrisma.maintenanceRequest.findMany).toHaveBeenCalledWith({ - where: { housingUnitId: 'hu-1' }, - include: { + expect(mockFindMany).toHaveBeenCalledWith({ + where: eq(maintenanceRequest.housingUnitId, 'hu-1'), + with: { spot: true, - reportedBy: { select: { id: true, code: true } }, + reportedBy: { columns: { id: true, code: true } }, }, - orderBy: { createdAt: 'desc' }, - take: 20, + orderBy: [desc(maintenanceRequest.createdAt)], + limit: 20, }) }) it('returns empty array when no requests exist', async () => { - ;(mockPrisma.maintenanceRequest.findMany as jest.Mock).mockResolvedValue([]) + mockFindMany.mockResolvedValue([]) const result = await getHousingUnitMaintenance('hu-1') diff --git a/src/lib/actions/__tests__/marketplace.test.ts b/src/lib/actions/__tests__/marketplace.test.ts index 3cc19cdd..03249f54 100644 --- a/src/lib/actions/__tests__/marketplace.test.ts +++ b/src/lib/actions/__tests__/marketplace.test.ts @@ -7,7 +7,8 @@ * half of the board sorts into the wrong list forever. None of it throws. */ -import { prisma } from '@/lib/db' +import { marketplacePost } from '@/lib/db' +import { and, eq, inArray } from 'drizzle-orm' import { getPortalAuth } from '@/lib/portal-auth' import { claimMarketplacePost, @@ -19,16 +20,46 @@ import { reopenMarketplacePost, } from '../marketplace' +const mockFindFirst = jest.fn() +const mockFindMany = jest.fn() +// Receives the insert payload of db.insert(...).values(payload). +const mockInsert = jest.fn() +// Receives (set payload, where expression) of a plain awaited update. +const mockUpdate = jest.fn() +// Receives (set payload, where expression) of an update awaited via .returning(); +// resolves with the returned rows array. +const mockUpdateReturning = jest.fn() +// Receives the where expression of db.delete(...).where(where). +const mockDelete = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - marketplacePost: { - create: jest.fn(), - findUnique: jest.fn(), - findMany: jest.fn(), - update: jest.fn(), - updateMany: jest.fn(), - delete: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + marketplacePost: { + findFirst: (...a: unknown[]) => mockFindFirst(...a), + findMany: (...a: unknown[]) => mockFindMany(...a), + }, }, + insert: jest.fn(() => ({ + values: (v: unknown): Promise => Promise.resolve(mockInsert(v)), + })), + update: jest.fn(() => ({ + set: (v: unknown) => ({ + // The same builder is either awaited directly or via .returning(); + // record only the path the code actually takes. + where: (w: unknown) => ({ + then: ( + resolve: (value: unknown) => unknown, + reject: (reason: unknown) => unknown, + ): Promise => Promise.resolve(mockUpdate(v, w)).then(resolve, reject), + returning: (): Promise => Promise.resolve(mockUpdateReturning(v, w)), + }), + }), + })), + delete: jest.fn(() => ({ + where: (w: unknown): Promise => Promise.resolve(mockDelete(w)), + })), }, })) @@ -36,16 +67,6 @@ jest.mock('next/cache', () => ({ revalidatePath: jest.fn() })) jest.mock('@/lib/portal-auth', () => ({ getPortalAuth: jest.fn() })) jest.mock('@/lib/auth', () => ({ getCurrentUser: jest.fn() })) -const mockPrisma = prisma as unknown as { - marketplacePost: { - create: jest.Mock - findUnique: jest.Mock - findMany: jest.Mock - update: jest.Mock - updateMany: jest.Mock - delete: jest.Mock - } -} const mockAuth = getPortalAuth as jest.MockedFunction const ME = 'resident-me' @@ -96,7 +117,7 @@ describe('claiming', () => { // Nothing stopped this, and the result was a listing marked "Übernommen // von ": off the open board, unreachable by // anyone who actually wanted it, and indistinguishable from a real match. - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', hiddenByStaff: false, @@ -107,44 +128,45 @@ describe('claiming', () => { const result = await claimMarketplacePost(form({ id: 'post-1' })) expect(result.success).toBe(false) - expect(mockPrisma.marketplacePost.updateMany).not.toHaveBeenCalled() + expect(mockUpdateReturning).not.toHaveBeenCalled() }) it('claims somebody else’s open post', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', hiddenByStaff: false, postedById: OTHER, claimedById: null, }) - mockPrisma.marketplacePost.updateMany.mockResolvedValue({ count: 1 }) + mockUpdateReturning.mockResolvedValue([{ id: 'post-1' }]) const result = await claimMarketplacePost(form({ id: 'post-1' })) expect(result.success).toBe(true) // Conditional on still being OPEN, so two people pressing at the same // moment produce one winner rather than a silent overwrite. - expect(mockPrisma.marketplacePost.updateMany).toHaveBeenCalledWith( - expect.objectContaining({ where: { id: 'post-1', status: 'OPEN' } }), + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ status: 'CLAIMED', claimedById: ME }), + and(eq(marketplacePost.id, 'post-1'), eq(marketplacePost.status, 'OPEN')), ) }) it('loses the race rather than overwriting the winner', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', hiddenByStaff: false, postedById: OTHER, claimedById: null, }) - mockPrisma.marketplacePost.updateMany.mockResolvedValue({ count: 0 }) + mockUpdateReturning.mockResolvedValue([]) expect((await claimMarketplacePost(form({ id: 'post-1' }))).success).toBe(false) }) it('refuses a post staff have hidden', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', hiddenByStaff: true, @@ -160,7 +182,7 @@ describe('backing out', () => { it('lets the claimer release, putting the post back on the board', async () => { // The only way out used to be CLOSE, which takes the item off the board // entirely — one person's second thoughts destroyed the offer for everyone. - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'CLAIMED', postedById: OTHER, @@ -170,15 +192,14 @@ describe('backing out', () => { const result = await releaseMarketplaceClaim(form({ id: 'post-1' })) expect(result.success).toBe(true) - expect(mockPrisma.marketplacePost.update).toHaveBeenCalledWith( - expect.objectContaining({ - data: { status: 'OPEN', claimedById: null, claimedAt: null }, - }), + expect(mockUpdate).toHaveBeenCalledWith( + { status: 'OPEN', claimedById: null, claimedAt: null }, + eq(marketplacePost.id, 'post-1'), ) }) it('lets the poster release a claimer who never turned up', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'CLAIMED', postedById: ME, @@ -189,7 +210,7 @@ describe('backing out', () => { }) it('refuses a release from a bystander', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'CLAIMED', postedById: OTHER, @@ -197,13 +218,13 @@ describe('backing out', () => { }) expect((await releaseMarketplaceClaim(form({ id: 'post-1' }))).success).toBe(false) - expect(mockPrisma.marketplacePost.update).not.toHaveBeenCalled() + expect(mockUpdate).not.toHaveBeenCalled() }) }) describe('withdrawing', () => { it('deletes your own untouched post', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', postedById: ME, @@ -211,13 +232,13 @@ describe('withdrawing', () => { }) expect((await deleteMarketplacePost(form({ id: 'post-1' }))).success).toBe(true) - expect(mockPrisma.marketplacePost.delete).toHaveBeenCalled() + expect(mockDelete).toHaveBeenCalled() }) it('refuses to delete a post somebody has already answered', async () => { // Deleting it would erase the other person's side of an arrangement // without telling them. A claimed post can only be closed. - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'CLAIMED', postedById: ME, @@ -225,11 +246,11 @@ describe('withdrawing', () => { }) expect((await deleteMarketplacePost(form({ id: 'post-1' }))).success).toBe(false) - expect(mockPrisma.marketplacePost.delete).not.toHaveBeenCalled() + expect(mockDelete).not.toHaveBeenCalled() }) it('refuses to delete somebody else’s post', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'OPEN', postedById: OTHER, @@ -240,7 +261,7 @@ describe('withdrawing', () => { }) it('reopens only for the poster', async () => { - mockPrisma.marketplacePost.findUnique.mockResolvedValue({ + mockFindFirst.mockResolvedValue({ id: 'post-1', status: 'CLOSED', postedById: OTHER, @@ -257,10 +278,8 @@ describe('posting', () => { form({ title: 'Sofa', description: 'Rot', kind: 'GIVE_AWAY', category: 'FURNITURE' }), ) - expect(mockPrisma.marketplacePost.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ kind: 'GIVE_AWAY', category: 'FURNITURE' }), - }), + expect(mockInsert).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'GIVE_AWAY', category: 'FURNITURE' }), ) }) @@ -278,10 +297,8 @@ describe('posting', () => { }), ) - expect(mockPrisma.marketplacePost.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ kind: 'OFFER_HELP', category: 'OTHER' }), - }), + expect(mockInsert).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'OFFER_HELP', category: 'OTHER' }), ) }) @@ -291,7 +308,7 @@ describe('posting', () => { ) expect(result.success).toBe(false) - expect(mockPrisma.marketplacePost.create).not.toHaveBeenCalled() + expect(mockInsert).not.toHaveBeenCalled() }) }) @@ -299,7 +316,7 @@ describe('reading the board', () => { it('hands the contact note only to the two people the handover is between', async () => { // The payload is the leak, not the markup: a note dropped in the JSX still // ships to every browser that renders the page. - mockPrisma.marketplacePost.findMany.mockResolvedValue([ + mockFindMany.mockResolvedValue([ row({ id: 'theirs', postedById: OTHER, claimedById: null }), row({ id: 'mine', postedById: ME, claimedById: null }), row({ id: 'claimed-by-me', postedById: OTHER, claimedById: ME }), @@ -314,7 +331,7 @@ describe('reading the board', () => { }) it('shows other units only what is still open', async () => { - mockPrisma.marketplacePost.findMany.mockResolvedValue([ + mockFindMany.mockResolvedValue([ row({ id: 'other-open', housingUnit: { id: 'unit-x', code: 'X' }, status: 'OPEN' }), row({ id: 'other-closed', housingUnit: { id: 'unit-x', code: 'X' }, status: 'CLOSED' }), row({ id: 'own-closed', status: 'CLOSED' }), @@ -328,7 +345,7 @@ describe('reading the board', () => { }) it('filters to one half of the board when asked', async () => { - mockPrisma.marketplacePost.findMany.mockResolvedValue([ + mockFindMany.mockResolvedValue([ row({ id: 'thing', kind: 'GIVE_AWAY' }), row({ id: 'help', kind: 'OFFER_HELP' }), ]) @@ -350,20 +367,22 @@ describe('reading the board', () => { */ describe('your own posts, for the dashboard', () => { it('asks only for your own posts that are still live', async () => { - mockPrisma.marketplacePost.findMany.mockResolvedValue([]) + mockFindMany.mockResolvedValue([]) await listMyMarketplacePosts() - const [args] = mockPrisma.marketplacePost.findMany.mock.calls[0] - expect(args.where).toEqual({ - postedById: ME, - hiddenByStaff: false, - status: { in: ['OPEN', 'CLAIMED'] }, - }) + const [args] = mockFindMany.mock.calls[0] + expect(args.where).toEqual( + and( + eq(marketplacePost.postedById, ME), + eq(marketplacePost.hiddenByStaff, false), + inArray(marketplacePost.status, ['OPEN', 'CLAIMED']), + ), + ) }) it('carries the contact note, which is the point of the card', async () => { - mockPrisma.marketplacePost.findMany.mockResolvedValue([ + mockFindMany.mockResolvedValue([ row({ postedById: ME, status: 'CLAIMED', @@ -384,6 +403,6 @@ describe('your own posts, for the dashboard', () => { mockAuth.mockResolvedValue(null as unknown as Awaited>) await expect(listMyMarketplacePosts()).resolves.toEqual([]) - expect(mockPrisma.marketplacePost.findMany).not.toHaveBeenCalled() + expect(mockFindMany).not.toHaveBeenCalled() }) }) diff --git a/src/lib/actions/__tests__/matching.test.ts b/src/lib/actions/__tests__/matching.test.ts index 65bc2a21..2a0e1d91 100644 --- a/src/lib/actions/__tests__/matching.test.ts +++ b/src/lib/actions/__tests__/matching.test.ts @@ -1,11 +1,11 @@ /** * Unit tests for matching server action: placeResident * - * placeResident takes FormData, runs a Prisma transaction, and calls redirect() on success. + * placeResident takes FormData, runs a db transaction, and calls redirect() on success. * Since redirect() throws by design in Next.js, we mock it to throw a sentinel error. */ -import { prisma } from '@/lib/db' +import { db, housingUnit, placementSpot, resident } from '@/lib/db' import { logAudit } from '@/lib/audit' import { calculateApartmentFit } from '@/lib/compatibility/aggregate' import { calculateCompatibility } from '@/lib/compatibility' @@ -17,27 +17,9 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // ============================================================================= jest.mock('@/lib/db', () => ({ - prisma: { - $transaction: jest.fn(), - resident: { - findUnique: jest.fn(), - update: jest.fn(), - }, - placement: { - findMany: jest.fn(), - create: jest.fn(), - }, - placementSpot: { - findUnique: jest.fn(), - update: jest.fn(), - }, - housingUnit: { - findUnique: jest.fn(), - update: jest.fn(), - }, - compatibilityAssessment: { - upsert: jest.fn(), - }, + ...jest.requireActual('@/lib/db'), + db: { + transaction: jest.fn(), }, })) @@ -151,7 +133,7 @@ jest.mock('@/lib/compatibility/placement-scores', () => ({ }), })) -const mockPrisma = prisma as jest.Mocked +const mockDb = db as unknown as { transaction: jest.Mock } beforeEach(() => { jest.clearAllMocks() @@ -162,8 +144,10 @@ beforeEach(() => { // ============================================================================= /** - * Configures prisma.$transaction to execute the callback with a mock tx object. - * Each mock method on tx is configurable via the txSetup callback. + * Configures db.transaction to execute the callback with a mock tx object. + * Each mock method on tx is configurable via the txSetup callback; a + * drizzle-shaped facade maps the source's tx.query/insert/update calls onto + * those holder mocks (updates dispatch on the real table object identity). */ function setupTransaction(txSetup: (tx: Record>) => void) { const tx: Record> = { @@ -174,11 +158,40 @@ function setupTransaction(txSetup: (tx: Record compatibilityAssessment: { upsert: jest.fn() }, } txSetup(tx) - ;(mockPrisma.$transaction as jest.Mock).mockImplementation( - async (cb: (tx: unknown) => unknown) => { - return cb(tx) + const drizzleTx = { + query: { + placementSpot: { findFirst: (...a: unknown[]) => tx.placementSpot.findUnique(...a) }, + resident: { findFirst: (...a: unknown[]) => tx.resident.findUnique(...a) }, + placement: { findMany: (...a: unknown[]) => tx.placement.findMany(...a) }, + housingUnit: { findFirst: (...a: unknown[]) => tx.housingUnit.findUnique(...a) }, }, - ) + // Only the placement table is inserted into in this action + insert: (_table: unknown) => ({ + values: (v: unknown) => ({ + returning: async (): Promise => [await tx.placement.create(v)], + }), + }), + update: (table: unknown) => ({ + set: (v: unknown) => ({ + where: (_w: unknown) => { + const m = + table === placementSpot + ? tx.placementSpot.update + : table === resident + ? tx.resident.update + : table === housingUnit + ? tx.housingUnit.update + : jest.fn() + return m(v) + }, + }), + }), + // Consumed only by the mocked saveBidirectionalAssessment above + compatibilityAssessment: tx.compatibilityAssessment, + } + mockDb.transaction.mockImplementation(async (cb: (tx: unknown) => unknown) => { + return cb(drizzleTx) + }) return tx } @@ -358,8 +371,8 @@ describe('placeResident', () => { await expect(placeResident(makeFormData())).rejects.toThrow('NEXT_REDIRECT') // Verify placement was created with computed scores - expect(tx.placement.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(tx.placement.create).toHaveBeenCalledWith( + expect.objectContaining({ residentId: 'res-1', housingUnitId: 'hu-1', spotId: 'spot-1', @@ -370,19 +383,13 @@ describe('placeResident', () => { practicalScore: 75, riskScore: 65, }), - }) + ) // Spot updated to OCCUPIED - expect(tx.placementSpot.update).toHaveBeenCalledWith({ - where: { id: 'spot-1' }, - data: { status: 'OCCUPIED' }, - }) + expect(tx.placementSpot.update).toHaveBeenCalledWith({ status: 'OCCUPIED' }) // Resident updated to PLACED - expect(tx.resident.update).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: { status: 'PLACED' }, - }) + expect(tx.resident.update).toHaveBeenCalledWith({ status: 'PLACED' }) // Unit NOT marked FULL (1 of 3 beds) expect(tx.housingUnit.update).not.toHaveBeenCalled() @@ -493,10 +500,7 @@ describe('placeResident', () => { await expect(placeResident(makeFormData())).rejects.toThrow('NEXT_REDIRECT') - expect(tx.housingUnit.update).toHaveBeenCalledWith({ - where: { id: 'hu-1' }, - data: { status: 'FULL' }, - }) + expect(tx.housingUnit.update).toHaveBeenCalledWith({ status: 'FULL' }) }) // --------------------------------------------------------------------------- @@ -583,11 +587,11 @@ describe('placeResident', () => { expect(tx.placementSpot.update).not.toHaveBeenCalled() // Placement created with null spotId - expect(tx.placement.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(tx.placement.create).toHaveBeenCalledWith( + expect.objectContaining({ spotId: null, }), - }) + ) }) }) diff --git a/src/lib/actions/__tests__/opportunities-portal.test.ts b/src/lib/actions/__tests__/opportunities-portal.test.ts index 2c043226..1457f6bf 100644 --- a/src/lib/actions/__tests__/opportunities-portal.test.ts +++ b/src/lib/actions/__tests__/opportunities-portal.test.ts @@ -9,20 +9,29 @@ * screenshot — so it is pinned here instead. */ -import { prisma } from '@/lib/db' -import { Prisma } from '@prisma/client' +import { DatabaseError } from 'pg' import { getResidentCookie } from '@/lib/portal-auth' import { expressInterest, withdrawInterest } from '../opportunities' +const mockResidentFindFirst = jest.fn() +const mockOpportunityFindFirst = jest.fn() +const mockApplicationFindFirst = jest.fn() +const mockApplicationCreate = jest.fn() +const mockApplicationDelete = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - resident: { findUnique: jest.fn() }, - opportunity: { findUnique: jest.fn() }, - opportunityApplication: { - create: jest.fn(), - findUnique: jest.fn(), - delete: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { findFirst: (...a: unknown[]) => mockResidentFindFirst(...a) }, + opportunity: { findFirst: (...a: unknown[]) => mockOpportunityFindFirst(...a) }, + opportunityApplication: { + findFirst: (...a: unknown[]) => mockApplicationFindFirst(...a), + }, }, + // `await db.insert(t).values(v)` — the source awaits without .returning() + insert: jest.fn(() => ({ values: (v: unknown) => mockApplicationCreate(v) })), + delete: jest.fn(() => ({ where: (w: unknown) => mockApplicationDelete(w) })), }, })) @@ -53,9 +62,15 @@ jest.mock('next/navigation', () => ({ }), })) -const mockPrisma = prisma as jest.Mocked const mockCookie = getResidentCookie as jest.MockedFunction +/** The pg error shape isUniqueViolation() recognizes (SQLSTATE 23505). */ +function uniqueViolation(): Error { + const error = new DatabaseError('duplicate', 0, 'error') + error.code = '23505' + return error +} + /** Run an action and report where it sent the resident. */ async function outcomeOf(run: () => Promise): Promise { try { @@ -78,12 +93,14 @@ const RESIDENT = { id: 'res-1', code: 'RES-AAA111' } beforeEach(() => { jest.clearAllMocks() mockCookie.mockResolvedValue(RESIDENT.code) - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ id: RESIDENT.id }) + mockResidentFindFirst.mockResolvedValue({ id: RESIDENT.id }) + mockApplicationCreate.mockResolvedValue(undefined) + mockApplicationDelete.mockResolvedValue(undefined) }) describe('expressInterest', () => { it('attaches the resident to a published listing', async () => { - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: 3, @@ -93,21 +110,21 @@ describe('expressInterest', () => { const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) expect(to).toBe('/portal/opportunities?ok=interest') - expect(mockPrisma.opportunityApplication.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockApplicationCreate).toHaveBeenCalledWith( + expect.objectContaining({ opportunityId: 'opp-1', residentId: RESIDENT.id, stage: 'INTERESTED', createdBy: 'RESIDENT', }), - }) + ) }) it('leaves the staff slot empty, because no member of staff has picked it up', async () => { // The unclaimed queue is filtered on exactly this. Filling it with the // resident's own action would make every self-registered interest look // like it already has someone working on it. - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: null, @@ -116,12 +133,12 @@ describe('expressInterest', () => { await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) - const data = (mockPrisma.opportunityApplication.create as jest.Mock).mock.calls[0][0].data + const data = mockApplicationCreate.mock.calls[0][0] expect(data.supportedByUserId).toBeUndefined() }) it.each(['DRAFT', 'ARCHIVED'])('refuses a %s listing', async (status) => { - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status, seats: 5, @@ -131,11 +148,11 @@ describe('expressInterest', () => { const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) expect(to).toBe('/portal/opportunities?error=unavailable') - expect(mockPrisma.opportunityApplication.create).not.toHaveBeenCalled() + expect(mockApplicationCreate).not.toHaveBeenCalled() }) it('refuses a listing whose seats are already taken', async () => { - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: 2, @@ -145,13 +162,13 @@ describe('expressInterest', () => { const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) expect(to).toBe('/portal/opportunities?error=full') - expect(mockPrisma.opportunityApplication.create).not.toHaveBeenCalled() + expect(mockApplicationCreate).not.toHaveBeenCalled() }) it('still has room when the seats it holds are finished ones', async () => { // ENDED and DECLINED do not occupy a place. Counting them would retire a // listing permanently after enough people had passed through it. - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: 2, @@ -164,18 +181,13 @@ describe('expressInterest', () => { }) it('treats a second tap as the success it already is', async () => { - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: null, applications: [], }) - ;(mockPrisma.opportunityApplication.create as jest.Mock).mockRejectedValue( - new Prisma.PrismaClientKnownRequestError('duplicate', { - code: 'P2002', - clientVersion: 'test', - }), - ) + mockApplicationCreate.mockRejectedValue(uniqueViolation()) const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) @@ -185,13 +197,13 @@ describe('expressInterest', () => { it('reports a real database failure as a failure', async () => { // The counterpart to the test above: swallowing every error would make the // duplicate tolerance quietly hide genuine breakage too. - ;(mockPrisma.opportunity.findUnique as jest.Mock).mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ id: 'opp-1', status: 'PUBLISHED', seats: null, applications: [], }) - ;(mockPrisma.opportunityApplication.create as jest.Mock).mockRejectedValue(new Error('boom')) + mockApplicationCreate.mockRejectedValue(new Error('boom')) const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) @@ -204,13 +216,13 @@ describe('expressInterest', () => { const to = await outcomeOf(() => expressInterest(form({ opportunityId: 'opp-1' }))) expect(to).toBe('/login') - expect(mockPrisma.opportunity.findUnique).not.toHaveBeenCalled() + expect(mockOpportunityFindFirst).not.toHaveBeenCalled() }) }) describe('withdrawInterest', () => { it('removes an untouched interest the resident registered themselves', async () => { - ;(mockPrisma.opportunityApplication.findUnique as jest.Mock).mockResolvedValue({ + mockApplicationFindFirst.mockResolvedValue({ id: 'app-1', residentId: RESIDENT.id, opportunityId: 'opp-1', @@ -221,16 +233,14 @@ describe('withdrawInterest', () => { const to = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) expect(to).toBe('/portal/opportunities?ok=withdrawn') - expect(mockPrisma.opportunityApplication.delete).toHaveBeenCalledWith({ - where: { id: 'app-1' }, - }) + expect(mockApplicationDelete).toHaveBeenCalledTimes(1) }) it('will not delete another resident row, and does not admit that it exists', async () => { // Same answer as for an id that is not in the table at all. A different // message would confirm to the holder of a guessed id that some other // resident has applied for something. - ;(mockPrisma.opportunityApplication.findUnique as jest.Mock).mockResolvedValue({ + mockApplicationFindFirst.mockResolvedValue({ id: 'app-1', residentId: 'someone-else', opportunityId: 'opp-1', @@ -240,15 +250,15 @@ describe('withdrawInterest', () => { const foreign = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) - ;(mockPrisma.opportunityApplication.findUnique as jest.Mock).mockResolvedValue(null) + mockApplicationFindFirst.mockResolvedValue(null) const missing = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) expect(foreign).toBe(missing) - expect(mockPrisma.opportunityApplication.delete).not.toHaveBeenCalled() + expect(mockApplicationDelete).not.toHaveBeenCalled() }) it('will not delete a row staff created', async () => { - ;(mockPrisma.opportunityApplication.findUnique as jest.Mock).mockResolvedValue({ + mockApplicationFindFirst.mockResolvedValue({ id: 'app-1', residentId: RESIDENT.id, opportunityId: 'opp-1', @@ -259,7 +269,7 @@ describe('withdrawInterest', () => { const to = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) expect(to).toBe('/portal/opportunities?error=locked') - expect(mockPrisma.opportunityApplication.delete).not.toHaveBeenCalled() + expect(mockApplicationDelete).not.toHaveBeenCalled() }) it.each(['APPLIED', 'INTERVIEW', 'ACCEPTED', 'STARTED', 'ENDED'])( @@ -267,7 +277,7 @@ describe('withdrawInterest', () => { async (stage) => { // Past INTERESTED a conversation has happened, and the row is staff's // record of it as much as the resident's. - ;(mockPrisma.opportunityApplication.findUnique as jest.Mock).mockResolvedValue({ + mockApplicationFindFirst.mockResolvedValue({ id: 'app-1', residentId: RESIDENT.id, opportunityId: 'opp-1', @@ -278,7 +288,7 @@ describe('withdrawInterest', () => { const to = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) expect(to).toBe('/portal/opportunities?error=locked') - expect(mockPrisma.opportunityApplication.delete).not.toHaveBeenCalled() + expect(mockApplicationDelete).not.toHaveBeenCalled() }, ) @@ -288,6 +298,6 @@ describe('withdrawInterest', () => { const to = await outcomeOf(() => withdrawInterest(form({ applicationId: 'app-1' }))) expect(to).toBe('/login') - expect(mockPrisma.opportunityApplication.findUnique).not.toHaveBeenCalled() + expect(mockApplicationFindFirst).not.toHaveBeenCalled() }) }) diff --git a/src/lib/actions/__tests__/placements.test.ts b/src/lib/actions/__tests__/placements.test.ts index 4bca2f57..8bd02069 100644 --- a/src/lib/actions/__tests__/placements.test.ts +++ b/src/lib/actions/__tests__/placements.test.ts @@ -5,7 +5,8 @@ * endPlacement and transferPlacement use redirect() which throws, so they are not tested here. */ -import { prisma } from '@/lib/db' +import { getTableName } from 'drizzle-orm' +import { housingUnit, placementSpot, resident } from '@/lib/db' import { logAudit } from '@/lib/audit' import { createPlacement } from '../placements' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -14,30 +15,12 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockTransaction = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - $transaction: jest.fn(), - resident: { - findUnique: jest.fn(), - update: jest.fn(), - }, - placement: { - findFirst: jest.fn(), - findMany: jest.fn(), - create: jest.fn(), - update: jest.fn(), - }, - placementSpot: { - findUnique: jest.fn(), - update: jest.fn(), - }, - housingUnit: { - findUnique: jest.fn(), - update: jest.fn(), - }, - compatibilityAssessment: { - upsert: jest.fn(), - }, + ...jest.requireActual('@/lib/db'), + db: { + transaction: (fn: (tx: unknown) => unknown) => mockTransaction(fn), }, })) @@ -101,6 +84,7 @@ jest.mock('@/lib/compatibility', () => ({ strengths: [], concerns: [], }), + saveBidirectionalAssessment: jest.fn(), })) jest.mock('@/lib/compatibility/convert', () => ({ @@ -117,34 +101,61 @@ jest.mock('@/lib/compatibility/placement-scores', () => ({ }), })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) // ============================================================================= -// Helper to set up $transaction mock +// Helper to set up db.transaction mock // ============================================================================= +interface MockTx { + query: { + resident: { findFirst: jest.Mock } + placement: { findFirst: jest.Mock; findMany: jest.Mock } + placementSpot: { findFirst: jest.Mock } + housingUnit: { findFirst: jest.Mock } + } + /** Resolves the rows returned by tx.insert(…).values(…).returning() */ + insertReturning: jest.Mock + /** Records every tx.update(table).set(payload) as (tableName, payload) */ + updateSet: jest.Mock +} + /** - * Configures prisma.$transaction to execute the callback with a mock tx object. + * Configures db.transaction to execute the callback with a mock tx object. * Each mock method on tx is configurable via the txSetup callback. */ -function setupTransaction(txSetup: (tx: Record>) => void) { - const tx: Record> = { - resident: { findUnique: jest.fn(), update: jest.fn() }, - placement: { findFirst: jest.fn(), findMany: jest.fn(), create: jest.fn() }, - placementSpot: { findUnique: jest.fn(), update: jest.fn() }, - housingUnit: { findUnique: jest.fn(), update: jest.fn() }, - compatibilityAssessment: { upsert: jest.fn() }, +function setupTransaction(txSetup: (tx: MockTx) => void) { + const tx: MockTx = { + query: { + resident: { findFirst: jest.fn() }, + placement: { findFirst: jest.fn(), findMany: jest.fn() }, + placementSpot: { findFirst: jest.fn() }, + housingUnit: { findFirst: jest.fn() }, + }, + insertReturning: jest.fn().mockResolvedValue([{}]), + updateSet: jest.fn(), } txSetup(tx) - ;(mockPrisma.$transaction as jest.Mock).mockImplementation( - async (cb: (tx: unknown) => unknown) => { - return cb(tx) - }, - ) + + const txSurface = { + query: tx.query, + insert: jest.fn(() => ({ + values: (v: unknown) => ({ + returning: (): Promise => tx.insertReturning(v), + }), + })), + update: jest.fn((table: unknown) => ({ + set: (v: unknown) => { + tx.updateSet(getTableName(table as any), v) + return { where: () => Promise.resolve([]) } + }, + })), + } + mockTransaction.mockImplementation(async (cb: (t: unknown) => unknown) => { + return cb(txSurface) + }) return tx } @@ -163,7 +174,7 @@ const baseInput = { describe('createPlacement', () => { it('returns error when resident not found', async () => { setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue(null) + tx.query.resident.findFirst.mockResolvedValue(null) }) const result = await createPlacement(baseInput) @@ -175,8 +186,8 @@ describe('createPlacement', () => { it('returns error when resident already has active placement', async () => { setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) - tx.placement.findFirst.mockResolvedValue({ id: 'pl-existing', status: 'ACTIVE' }) + tx.query.resident.findFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + tx.query.placement.findFirst.mockResolvedValue({ id: 'pl-existing', status: 'ACTIVE' }) }) const result = await createPlacement(baseInput) @@ -187,9 +198,9 @@ describe('createPlacement', () => { it('returns error when spot not found', async () => { setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) - tx.placement.findFirst.mockResolvedValue(null) - tx.placementSpot.findUnique.mockResolvedValue(null) + tx.query.resident.findFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + tx.query.placement.findFirst.mockResolvedValue(null) + tx.query.placementSpot.findFirst.mockResolvedValue(null) }) const result = await createPlacement(baseInput) @@ -200,9 +211,9 @@ describe('createPlacement', () => { it('returns error when spot is not available', async () => { setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) - tx.placement.findFirst.mockResolvedValue(null) - tx.placementSpot.findUnique.mockResolvedValue({ id: 'spot-1', status: 'OCCUPIED' }) + tx.query.resident.findFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + tx.query.placement.findFirst.mockResolvedValue(null) + tx.query.placementSpot.findFirst.mockResolvedValue({ id: 'spot-1', status: 'OCCUPIED' }) }) const result = await createPlacement(baseInput) @@ -215,14 +226,12 @@ describe('createPlacement', () => { const newPlacement = { id: 'pl-new', residentId: 'res-1' } setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) - tx.placement.findFirst.mockResolvedValue(null) - tx.placementSpot.findUnique.mockResolvedValue({ id: 'spot-1', status: 'AVAILABLE' }) - tx.placement.findMany.mockResolvedValue([]) // no existing placements in unit - tx.placement.create.mockResolvedValue(newPlacement) - tx.placementSpot.update.mockResolvedValue({}) - tx.resident.update.mockResolvedValue({}) - tx.housingUnit.findUnique.mockResolvedValue({ + tx.query.resident.findFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + tx.query.placement.findFirst.mockResolvedValue(null) + tx.query.placementSpot.findFirst.mockResolvedValue({ id: 'spot-1', status: 'AVAILABLE' }) + tx.query.placement.findMany.mockResolvedValue([]) // no existing placements in unit + tx.insertReturning.mockResolvedValue([newPlacement]) + tx.query.housingUnit.findFirst.mockResolvedValue({ id: 'hu-1', spots: [{ id: 'spot-2', status: 'AVAILABLE' }], // still spots available }) @@ -245,28 +254,25 @@ describe('createPlacement', () => { const newPlacement = { id: 'pl-new', residentId: 'res-1' } const tx = setupTransaction((tx) => { - tx.resident.findUnique.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) - tx.placement.findFirst.mockResolvedValue(null) - tx.placementSpot.findUnique.mockResolvedValue({ id: 'spot-1', status: 'AVAILABLE' }) - tx.placement.findMany.mockResolvedValue([]) - tx.placement.create.mockResolvedValue(newPlacement) - tx.placementSpot.update.mockResolvedValue({}) - tx.resident.update.mockResolvedValue({}) + tx.query.resident.findFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001' }) + tx.query.placement.findFirst.mockResolvedValue(null) + tx.query.placementSpot.findFirst.mockResolvedValue({ id: 'spot-1', status: 'AVAILABLE' }) + tx.query.placement.findMany.mockResolvedValue([]) + tx.insertReturning.mockResolvedValue([newPlacement]) // No remaining available spots - tx.housingUnit.findUnique.mockResolvedValue({ + tx.query.housingUnit.findFirst.mockResolvedValue({ id: 'hu-1', spots: [], }) - tx.housingUnit.update.mockResolvedValue({}) }) const result = await createPlacement(baseInput) expect(result.success).toBe(true) - expect(tx.housingUnit.update).toHaveBeenCalledWith({ - where: { id: 'hu-1' }, - data: { status: 'FULL' }, - }) + expect(tx.updateSet).toHaveBeenCalledWith(getTableName(housingUnit), { status: 'FULL' }) + // spot occupied + resident placed still happen alongside the FULL update + expect(tx.updateSet).toHaveBeenCalledWith(getTableName(placementSpot), { status: 'OCCUPIED' }) + expect(tx.updateSet).toHaveBeenCalledWith(getTableName(resident), { status: 'PLACED' }) }) }) diff --git a/src/lib/actions/__tests__/residents.test.ts b/src/lib/actions/__tests__/residents.test.ts index e59e79f4..ce1db326 100644 --- a/src/lib/actions/__tests__/residents.test.ts +++ b/src/lib/actions/__tests__/residents.test.ts @@ -5,7 +5,8 @@ * createResident/updateResident use redirect() which throws, so they are not tested here. */ -import { prisma } from '@/lib/db' +import { resident, placement } from '@/lib/db' +import { eq } from 'drizzle-orm' import { logAudit } from '@/lib/audit' import { exitResident, @@ -19,20 +20,36 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockResidentFindFirst = jest.fn() +// Receives (set payload, where expression) of a resident update. +const mockResidentUpdate = jest.fn() +// Receives (table, where expression) of db.$count; resolves the count. +const mockCount = jest.fn() +// Receives the where expression of db.delete(resident).where(where). +const mockResidentDelete = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - resident: { - findUnique: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + resident: { findFirst: (...a: unknown[]) => mockResidentFindFirst(...a) }, }, - placement: { count: jest.fn() }, - incident: { count: jest.fn() }, - incidentInvolvement: { count: jest.fn() }, - maintenanceRequest: { count: jest.fn() }, - compatibilityAssessment: { count: jest.fn() }, - auditLog: { create: jest.fn() }, + update: jest.fn(() => ({ + set: (v: unknown) => ({ + where: (w: unknown) => ({ + then: ( + resolve: (value: unknown) => unknown, + reject: (reason: unknown) => unknown, + ): Promise => Promise.resolve(mockResidentUpdate(v, w)).then(resolve, reject), + returning: (): Promise => + Promise.resolve(mockResidentUpdate(v, w)).then((row: unknown) => [row]), + }), + }), + })), + delete: jest.fn(() => ({ + where: (w: unknown): Promise => Promise.resolve(mockResidentDelete(w)), + })), + $count: (...a: unknown[]) => mockCount(...a), }, })) @@ -48,13 +65,6 @@ jest.mock('@/lib/audit', () => ({ logAudit: jest.fn(), })) -const mockStaffUser = { - id: 'staff-1', - email: 'admin@test.com', - name: 'Test Admin', - role: 'ADMIN' as const, -} - jest.mock('@/lib/auth', () => ({ getCurrentUser: jest.fn().mockResolvedValue({ id: 'staff-1', @@ -86,8 +96,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -98,17 +106,17 @@ beforeEach(() => { describe('exitResident', () => { it('returns error when resident not found', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const result = await exitResident('nonexistent-id') expect(result).toEqual({ success: false, error: ERROR_MESSAGES.RESIDENT_NOT_FOUND }) - expect(mockPrisma.resident.update).not.toHaveBeenCalled() + expect(mockResidentUpdate).not.toHaveBeenCalled() expect(logAudit).not.toHaveBeenCalled() }) it('returns error when resident has active placements', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001', placements: [{ id: 'pl-1', status: 'ACTIVE' }], @@ -118,24 +126,21 @@ describe('exitResident', () => { expect(result.success).toBe(false) expect(result.error).toContain('aktive Platzierungen') - expect(mockPrisma.resident.update).not.toHaveBeenCalled() + expect(mockResidentUpdate).not.toHaveBeenCalled() }) it('succeeds and updates status to EXITED when no active placements', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001', placements: [], }) - ;(mockPrisma.resident.update as jest.Mock).mockResolvedValue({ id: 'res-1', status: 'EXITED' }) + mockResidentUpdate.mockResolvedValue({ id: 'res-1', status: 'EXITED' }) const result = await exitResident('res-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.resident.update).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: { status: 'EXITED' }, - }) + expect(mockResidentUpdate).toHaveBeenCalledWith({ status: 'EXITED' }, eq(resident.id, 'res-1')) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'END', @@ -146,8 +151,8 @@ describe('exitResident', () => { ) }) - it('returns error when prisma throws', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockRejectedValue(new Error('DB error')) + it('returns error when the db throws', async () => { + mockResidentFindFirst.mockRejectedValue(new Error('DB error')) const result = await exitResident('res-1') @@ -162,7 +167,7 @@ describe('exitResident', () => { describe('archiveResident', () => { it('returns error when resident not found', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const result = await archiveResident('nonexistent-id') @@ -170,7 +175,7 @@ describe('archiveResident', () => { }) it('returns error when resident has active placements', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', placements: [{ id: 'pl-1', status: 'ACTIVE' }], }) @@ -182,19 +187,16 @@ describe('archiveResident', () => { }) it('succeeds and sets status to EXITED', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', placements: [], }) - ;(mockPrisma.resident.update as jest.Mock).mockResolvedValue({ id: 'res-1' }) + mockResidentUpdate.mockResolvedValue({ id: 'res-1' }) const result = await archiveResident('res-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.resident.update).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: { status: 'EXITED' }, - }) + expect(mockResidentUpdate).toHaveBeenCalledWith({ status: 'EXITED' }, eq(resident.id, 'res-1')) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'ARCHIVE', @@ -211,7 +213,7 @@ describe('archiveResident', () => { describe('restoreResident', () => { it('returns error when resident not found', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const result = await restoreResident('nonexistent-id') @@ -219,35 +221,29 @@ describe('restoreResident', () => { }) it('restores to ACTIVE when no active placements', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', placements: [], }) - ;(mockPrisma.resident.update as jest.Mock).mockResolvedValue({ id: 'res-1' }) + mockResidentUpdate.mockResolvedValue({ id: 'res-1' }) const result = await restoreResident('res-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.resident.update).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: { status: 'ACTIVE' }, - }) + expect(mockResidentUpdate).toHaveBeenCalledWith({ status: 'ACTIVE' }, eq(resident.id, 'res-1')) }) it('restores to PLACED when resident has active placements', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', placements: [{ id: 'pl-1', status: 'ACTIVE' }], }) - ;(mockPrisma.resident.update as jest.Mock).mockResolvedValue({ id: 'res-1' }) + mockResidentUpdate.mockResolvedValue({ id: 'res-1' }) const result = await restoreResident('res-1') expect(result).toEqual({ success: true }) - expect(mockPrisma.resident.update).toHaveBeenCalledWith({ - where: { id: 'res-1' }, - data: { status: 'PLACED' }, - }) + expect(mockResidentUpdate).toHaveBeenCalledWith({ status: 'PLACED' }, eq(resident.id, 'res-1')) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'RESTORE', @@ -268,7 +264,7 @@ describe('hardDeleteResidentProtected', () => { const result = await hardDeleteResidentProtected('res-1', 'WRONG', 'Test deletion reason here') expect(result).toEqual({ success: false, error: 'Bestätigung fehlt (DELETE)' }) - expect(mockPrisma.resident.findUnique).not.toHaveBeenCalled() + expect(mockResidentFindFirst).not.toHaveBeenCalled() }) it('returns error when reason is too short', async () => { @@ -279,7 +275,7 @@ describe('hardDeleteResidentProtected', () => { }) it('returns error when resident not found', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue(null) + mockResidentFindFirst.mockResolvedValue(null) const result = await hardDeleteResidentProtected('res-1', 'DELETE', 'Testdaten bereinigen') @@ -287,7 +283,7 @@ describe('hardDeleteResidentProtected', () => { }) it('returns error when resident is not test/demo', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', code: 'RES-001', }) @@ -299,15 +295,12 @@ describe('hardDeleteResidentProtected', () => { }) it('returns error with blocker report when resident has linked history', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', code: 'test-resident-1', }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(2) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.incidentInvolvement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.maintenanceRequest.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.compatibilityAssessment.count as jest.Mock).mockResolvedValue(0) + // Two placements block the delete; every other linked table is empty. + mockCount.mockImplementation(async (table: unknown) => (table === placement ? 2 : 0)) const result = await hardDeleteResidentProtected('res-1', 'DELETE', 'Testdaten bereinigen') @@ -318,21 +311,17 @@ describe('hardDeleteResidentProtected', () => { }) it('succeeds for test resident with no linked history', async () => { - ;(mockPrisma.resident.findUnique as jest.Mock).mockResolvedValue({ + mockResidentFindFirst.mockResolvedValue({ id: 'res-1', code: 'test-resident-1', }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.incidentInvolvement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.maintenanceRequest.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.compatibilityAssessment.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.resident.delete as jest.Mock).mockResolvedValue({ id: 'res-1' }) + mockCount.mockResolvedValue(0) + mockResidentDelete.mockResolvedValue({ id: 'res-1' }) const result = await hardDeleteResidentProtected('res-1', 'DELETE', 'Testdaten bereinigen') expect(result).toEqual({ success: true }) - expect(mockPrisma.resident.delete).toHaveBeenCalledWith({ where: { id: 'res-1' } }) + expect(mockResidentDelete).toHaveBeenCalledWith(eq(resident.id, 'res-1')) expect(logAudit).toHaveBeenCalledWith( expect.objectContaining({ action: 'DELETE', diff --git a/src/lib/actions/__tests__/satisfaction.test.ts b/src/lib/actions/__tests__/satisfaction.test.ts index 71b9b14a..83419469 100644 --- a/src/lib/actions/__tests__/satisfaction.test.ts +++ b/src/lib/actions/__tests__/satisfaction.test.ts @@ -7,7 +7,8 @@ * createCheckInFromForm uses redirect() which throws, so we mock it to throw NEXT_REDIRECT. */ -import { prisma } from '@/lib/db' +import { placement, satisfactionCheckIn } from '@/lib/db' +import { eq, asc, desc } from 'drizzle-orm' import { logAudit } from '@/lib/audit' import { createCheckInFromForm, @@ -20,25 +21,41 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockPlacementFindFirst = jest.fn() +const mockCheckInFindMany = jest.fn() +// Receives the insert payload; resolves with the created row. +const mockCheckInInsert = jest.fn() +// Receives (table, set payload, where expression) of the placement update. +const mockPlacementUpdate = jest.fn() + jest.mock('@/lib/db', () => { - const prismaMock: { - placement: { findUnique: jest.Mock; update: jest.Mock } - satisfactionCheckIn: { create: jest.Mock; findMany: jest.Mock } - $transaction: jest.Mock - } = { - placement: { - findUnique: jest.fn(), - update: jest.fn(), - }, - satisfactionCheckIn: { - create: jest.fn(), - findMany: jest.fn(), + // Keep tables/enums/helpers real; only fake `db`. + const actual = jest.requireActual('@/lib/db') + // db.transaction(cb) invokes the callback with a tx carrying the builder + // surface the action uses, so the write mocks continue to be observed. + const tx = { + insert: jest.fn(() => ({ + values: (v: unknown) => ({ + returning: (): Promise => + Promise.resolve(mockCheckInInsert(v)).then((row: unknown) => [row]), + }), + })), + update: jest.fn((table: unknown) => ({ + set: (v: unknown) => ({ + where: (w: unknown): Promise => Promise.resolve(mockPlacementUpdate(table, v, w)), + }), + })), + } + return { + ...actual, + db: { + query: { + placement: { findFirst: (...a: unknown[]) => mockPlacementFindFirst(...a) }, + satisfactionCheckIn: { findMany: (...a: unknown[]) => mockCheckInFindMany(...a) }, + }, + transaction: (fn: (t: unknown) => unknown) => fn(tx), }, - // $transaction(callback) invokes the callback with the same mock client - // so individual model mocks (create, update) continue to be observed. - $transaction: jest.fn(async (cb: (tx: unknown) => Promise) => cb(prismaMock)), } - return { prisma: prismaMock } }) jest.mock('next/cache', () => ({ @@ -95,8 +112,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -142,7 +157,7 @@ describe('createCheckInFromForm', () => { }) it('throws when placement is not found', async () => { - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue(null) + mockPlacementFindFirst.mockResolvedValue(null) await expect(createCheckInFromForm(makeCheckInFormData())).rejects.toThrow( ERROR_MESSAGES.PLACEMENT_NOT_FOUND, @@ -151,19 +166,19 @@ describe('createCheckInFromForm', () => { it('creates check-in and redirects on success', async () => { const placementStart = new Date('2025-01-01') - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: placementStart, }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ + mockCheckInInsert.mockResolvedValue({ id: 'ci-1', }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockPlacementUpdate.mockResolvedValue({}) await expect(createCheckInFromForm(makeCheckInFormData())).rejects.toThrow('NEXT_REDIRECT') - expect(mockPrisma.satisfactionCheckIn.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockCheckInInsert).toHaveBeenCalledWith( + expect.objectContaining({ placementId: 'clxxxxxxxxxxxxxxxxx0001', checkInType: 'REGULAR', overallSatisfaction: 4, @@ -172,13 +187,14 @@ describe('createCheckInFromForm', () => { safetyFeeling: 5, isAnonymous: false, }), - }) + ) // Placement satisfaction updated - expect(mockPrisma.placement.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0001' }, - data: { satisfactionRating: 4 }, - }) + expect(mockPlacementUpdate).toHaveBeenCalledWith( + placement, + { satisfactionRating: 4 }, + eq(placement.id, 'clxxxxxxxxxxxxxxxxx0001'), + ) // Audit logged expect(logAudit).toHaveBeenCalledWith({ @@ -199,35 +215,35 @@ describe('createCheckInFromForm', () => { }) it('uses provided weekNumber when given', async () => { - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: new Date('2025-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ + mockCheckInInsert.mockResolvedValue({ id: 'ci-1', }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockPlacementUpdate.mockResolvedValue({}) const fd = makeCheckInFormData({ weekNumber: '5' }) await expect(createCheckInFromForm(fd)).rejects.toThrow('NEXT_REDIRECT') - expect(mockPrisma.satisfactionCheckIn.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockCheckInInsert).toHaveBeenCalledWith( + expect.objectContaining({ weekNumber: 5, }), - }) + ) }) it('includes concerns and text fields when provided', async () => { - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: new Date('2025-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ + mockCheckInInsert.mockResolvedValue({ id: 'ci-1', }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockPlacementUpdate.mockResolvedValue({}) const fd = makeCheckInFormData({ concerns: 'Noise at night', @@ -238,14 +254,14 @@ describe('createCheckInFromForm', () => { await expect(createCheckInFromForm(fd)).rejects.toThrow('NEXT_REDIRECT') - expect(mockPrisma.satisfactionCheckIn.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockCheckInInsert).toHaveBeenCalledWith( + expect.objectContaining({ concerns: 'Noise at night', improvements: 'Quiet hours', positives: 'Nice location', collectedBy: 'Staff member', }), - }) + ) // hasConcerns should be true in audit expect(logAudit).toHaveBeenCalledWith( @@ -257,12 +273,12 @@ describe('createCheckInFromForm', () => { ) }) - it('throws user-facing error when prisma create fails', async () => { - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + it('throws user-facing error when the insert fails', async () => { + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: new Date('2025-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockRejectedValue(new Error('DB error')) + mockCheckInInsert.mockRejectedValue(new Error('DB error')) await expect(createCheckInFromForm(makeCheckInFormData())).rejects.toThrow( ERROR_MESSAGES.CHECKIN_SAVE_ERROR, @@ -290,31 +306,31 @@ describe('createCheckInFromForm', () => { */ describe('who collected a check-in', () => { beforeEach(() => { - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: new Date('2025-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ id: 'ci-1' }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockCheckInInsert.mockResolvedValue({ id: 'ci-1' }) + mockPlacementUpdate.mockResolvedValue({}) }) it('always records the signed-in account, whatever the form says', async () => { for (const typed of ['', 'Team Nord', 'a colleague']) { jest.clearAllMocks() - ;(mockPrisma.placement.findUnique as jest.Mock).mockResolvedValue({ + mockPlacementFindFirst.mockResolvedValue({ residentId: 'res-1', startDate: new Date('2025-01-01'), }) - ;(mockPrisma.satisfactionCheckIn.create as jest.Mock).mockResolvedValue({ id: 'ci-1' }) - ;(mockPrisma.placement.update as jest.Mock).mockResolvedValue({}) + mockCheckInInsert.mockResolvedValue({ id: 'ci-1' }) + mockPlacementUpdate.mockResolvedValue({}) await expect( createCheckInFromForm(makeCheckInFormData({ collectedBy: typed })), ).rejects.toThrow('NEXT_REDIRECT') - const [call] = (mockPrisma.satisfactionCheckIn.create as jest.Mock).mock.calls - expect(call[0].data.collectedByUserId).toBe(mockStaffUser.id) - expect(call[0].data.collectedBy).toBe(typed || null) + const [call] = mockCheckInInsert.mock.calls + expect(call[0].collectedByUserId).toBe(mockStaffUser.id) + expect(call[0].collectedBy).toBe(typed || null) } }) @@ -323,9 +339,9 @@ describe('who collected a check-in', () => { createCheckInFromForm(makeCheckInFormData({ collectedBy: 'Team Nord' })), ).rejects.toThrow('NEXT_REDIRECT') - const [call] = (mockPrisma.satisfactionCheckIn.create as jest.Mock).mock.calls + const [call] = mockCheckInInsert.mock.calls // The note must never be mistaken for an identifier. - expect(call[0].data.collectedBy).not.toBe(call[0].data.collectedByUserId) + expect(call[0].collectedBy).not.toBe(call[0].collectedByUserId) }) }) @@ -339,19 +355,19 @@ describe('getPlacementCheckIns', () => { { id: 'ci-1', overallSatisfaction: 4 }, { id: 'ci-2', overallSatisfaction: 3 }, ] - ;(mockPrisma.satisfactionCheckIn.findMany as jest.Mock).mockResolvedValue(mockCheckIns) + mockCheckInFindMany.mockResolvedValue(mockCheckIns) const result = await getPlacementCheckIns('pl-1') expect(result).toEqual(mockCheckIns) - expect(mockPrisma.satisfactionCheckIn.findMany).toHaveBeenCalledWith({ - where: { placementId: 'pl-1' }, - orderBy: { createdAt: 'desc' }, + expect(mockCheckInFindMany).toHaveBeenCalledWith({ + where: eq(satisfactionCheckIn.placementId, 'pl-1'), + orderBy: [desc(satisfactionCheckIn.createdAt)], }) }) it('returns empty array when no check-ins exist', async () => { - ;(mockPrisma.satisfactionCheckIn.findMany as jest.Mock).mockResolvedValue([]) + mockCheckInFindMany.mockResolvedValue([]) const result = await getPlacementCheckIns('pl-1') @@ -367,7 +383,7 @@ describe('getPlacementSatisfactionTrend', () => { it('returns mapped trend data', async () => { const date1 = new Date('2025-01-15') const date2 = new Date('2025-01-22') - ;(mockPrisma.satisfactionCheckIn.findMany as jest.Mock).mockResolvedValue([ + mockCheckInFindMany.mockResolvedValue([ { createdAt: date1, weekNumber: 1, @@ -389,10 +405,10 @@ describe('getPlacementSatisfactionTrend', () => { { date: date2, week: 2, overall: 5, roommates: 4 }, ]) - expect(mockPrisma.satisfactionCheckIn.findMany).toHaveBeenCalledWith({ - where: { placementId: 'pl-1' }, - orderBy: { createdAt: 'asc' }, - select: { + expect(mockCheckInFindMany).toHaveBeenCalledWith({ + where: eq(satisfactionCheckIn.placementId, 'pl-1'), + orderBy: [asc(satisfactionCheckIn.createdAt)], + columns: { createdAt: true, weekNumber: true, overallSatisfaction: true, @@ -403,7 +419,7 @@ describe('getPlacementSatisfactionTrend', () => { it('handles null roommateRelations', async () => { const date1 = new Date('2025-01-15') - ;(mockPrisma.satisfactionCheckIn.findMany as jest.Mock).mockResolvedValue([ + mockCheckInFindMany.mockResolvedValue([ { createdAt: date1, weekNumber: 1, @@ -418,7 +434,7 @@ describe('getPlacementSatisfactionTrend', () => { }) it('returns empty array when no check-ins exist', async () => { - ;(mockPrisma.satisfactionCheckIn.findMany as jest.Mock).mockResolvedValue([]) + mockCheckInFindMany.mockResolvedValue([]) const result = await getPlacementSatisfactionTrend('pl-1') diff --git a/src/lib/actions/__tests__/spots.test.ts b/src/lib/actions/__tests__/spots.test.ts index f60b31ee..80f52947 100644 --- a/src/lib/actions/__tests__/spots.test.ts +++ b/src/lib/actions/__tests__/spots.test.ts @@ -2,10 +2,11 @@ * Unit tests for spots server actions * * Tests createSpot, updateSpot, deleteSpot, and createMultipleSpots. - * All actions take FormData, perform Prisma operations, and call revalidatePath. + * All actions take FormData, perform db operations, and call revalidatePath. */ -import { prisma } from '@/lib/db' +import { placementSpot, placement } from '@/lib/db' +import { and, eq } from 'drizzle-orm' import { createSpot, updateSpot, deleteSpot, createMultipleSpots } from '../spots' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -13,17 +14,46 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockInsertValues = jest.fn() +const mockUpdateReturning = jest.fn() +const mockUpdateWhere = jest.fn() +const mockDeleteWhere = jest.fn() +const mockCount = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - placementSpot: { - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - deleteMany: jest.fn(), - }, - placement: { - count: jest.fn(), - }, + ...jest.requireActual('@/lib/db'), + db: { + // `values()` is awaited directly for single inserts and `.returning()`ed for + // the room insert — expose both shapes over the same lazily-run mock. + insert: jest.fn(() => ({ + values: (v: unknown) => { + const run = () => mockInsertValues(v) as Promise + return { + then: (res?: never, rej?: never) => run().then(res, rej), + returning: () => run(), + } + }, + })), + update: jest.fn(() => ({ + set: (v: unknown) => ({ + where: (w: unknown) => { + mockUpdateWhere(w) + return { returning: (): Promise => mockUpdateReturning(v) } + }, + }), + })), + // `.where()` is awaited directly for the child-spot delete and + // `.returning()`ed for the spot itself. + delete: jest.fn(() => ({ + where: (w: unknown) => { + const run = () => mockDeleteWhere(w) as Promise + return { + then: (res?: never, rej?: never) => run().then(res, rej), + returning: () => run(), + } + }, + })), + $count: (...a: unknown[]) => mockCount(...a), }, })) @@ -77,8 +107,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -183,25 +211,21 @@ describe('createSpot', () => { }) it('creates a spot with required fields', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockResolvedValue({ - id: 'spot-1', - }) + mockInsertValues.mockResolvedValue([{ id: 'spot-1' }]) await createSpot(makeCreateSpotFormData()) - expect(mockPrisma.placementSpot.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenCalledWith( + expect.objectContaining({ housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', code: 'R101-B1', type: 'BED', }), - }) + ) }) it('creates a spot with optional fields', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockResolvedValue({ - id: 'spot-1', - }) + mockInsertValues.mockResolvedValue([{ id: 'spot-1' }]) const fd = makeCreateSpotFormData({ label: 'Bett 1', @@ -213,19 +237,19 @@ describe('createSpot', () => { await createSpot(fd) - expect(mockPrisma.placementSpot.create).toHaveBeenCalledWith({ - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenCalledWith( + expect.objectContaining({ label: 'Bett 1', squareMeters: 12, floor: 2, notes: 'Near window', status: 'AVAILABLE', }), - }) + ) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the insert fails', async () => { + mockInsertValues.mockRejectedValue(new Error('DB error')) await expect(createSpot(makeCreateSpotFormData())).rejects.toThrow( ERROR_MESSAGES.SPOT_CREATE_ERROR, @@ -245,42 +269,34 @@ describe('updateSpot', () => { }) it('updates a spot with new data', async () => { - ;(mockPrisma.placementSpot.update as jest.Mock).mockResolvedValue({ - id: 'clxxxxxxxxxxxxxxxxx0010', - }) + mockUpdateReturning.mockResolvedValue([{ id: 'clxxxxxxxxxxxxxxxxx0010' }]) await updateSpot(makeUpdateSpotFormData()) - expect(mockPrisma.placementSpot.update).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - data: expect.objectContaining({ + expect(mockUpdateWhere).toHaveBeenCalledWith(eq(placementSpot.id, 'clxxxxxxxxxxxxxxxxx0010')) + expect(mockUpdateReturning).toHaveBeenCalledWith( + expect.objectContaining({ code: 'R101-B1-updated', type: 'BED', parentSpotId: null, }), - }) + ) }) it('sets parentSpotId to null when not provided', async () => { - ;(mockPrisma.placementSpot.update as jest.Mock).mockResolvedValue({ - id: 'clxxxxxxxxxxxxxxxxx0010', - }) + mockUpdateReturning.mockResolvedValue([{ id: 'clxxxxxxxxxxxxxxxxx0010' }]) await updateSpot(makeUpdateSpotFormData()) - expect(mockPrisma.placementSpot.update).toHaveBeenCalledWith( + expect(mockUpdateReturning).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - parentSpotId: null, - }), + parentSpotId: null, }), ) }) it('preserves parentSpotId when provided', async () => { - ;(mockPrisma.placementSpot.update as jest.Mock).mockResolvedValue({ - id: 'clxxxxxxxxxxxxxxxxx0010', - }) + mockUpdateReturning.mockResolvedValue([{ id: 'clxxxxxxxxxxxxxxxxx0010' }]) const fd = makeUpdateSpotFormData({ parentSpotId: 'clxxxxxxxxxxxxxxxxx0099', @@ -288,17 +304,15 @@ describe('updateSpot', () => { await updateSpot(fd) - expect(mockPrisma.placementSpot.update).toHaveBeenCalledWith( + expect(mockUpdateReturning).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - parentSpotId: 'clxxxxxxxxxxxxxxxxx0099', - }), + parentSpotId: 'clxxxxxxxxxxxxxxxxx0099', }), ) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.placementSpot.update as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the update fails', async () => { + mockUpdateReturning.mockRejectedValue(new Error('DB error')) await expect(updateSpot(makeUpdateSpotFormData())).rejects.toThrow( ERROR_MESSAGES.SPOT_UPDATE_ERROR, @@ -318,7 +332,7 @@ describe('deleteSpot', () => { }) it('throws when spot has active placements', async () => { - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(1) + mockCount.mockResolvedValue(1) await expect(deleteSpot(makeDeleteSpotFormData())).rejects.toThrow( ERROR_MESSAGES.SPOT_DELETE_BLOCKED, @@ -326,29 +340,35 @@ describe('deleteSpot', () => { }) it('deletes child spots then the spot itself when no active placements', async () => { - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placementSpot.deleteMany as jest.Mock).mockResolvedValue({ count: 2 }) - ;(mockPrisma.placementSpot.delete as jest.Mock).mockResolvedValue({ - id: 'clxxxxxxxxxxxxxxxxx0010', - }) + mockCount.mockResolvedValue(0) + mockDeleteWhere.mockResolvedValue([{ id: 'clxxxxxxxxxxxxxxxxx0010' }]) await deleteSpot(makeDeleteSpotFormData()) + // The active-placement check scoped to this spot + expect(mockCount).toHaveBeenCalledWith( + placement, + and(eq(placement.spotId, 'clxxxxxxxxxxxxxxxxx0010'), eq(placement.status, 'ACTIVE')), + ) + // Child spots deleted first - expect(mockPrisma.placementSpot.deleteMany).toHaveBeenCalledWith({ - where: { parentSpotId: 'clxxxxxxxxxxxxxxxxx0010' }, - }) + expect(mockDeleteWhere).toHaveBeenNthCalledWith( + 1, + eq(placementSpot.parentSpotId, 'clxxxxxxxxxxxxxxxxx0010'), + ) // Then the spot itself - expect(mockPrisma.placementSpot.delete).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0010' }, - }) + expect(mockDeleteWhere).toHaveBeenNthCalledWith( + 2, + eq(placementSpot.id, 'clxxxxxxxxxxxxxxxxx0010'), + ) }) - it('throws user-facing error when prisma delete fails', async () => { - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placementSpot.deleteMany as jest.Mock).mockResolvedValue({ count: 0 }) - ;(mockPrisma.placementSpot.delete as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the delete fails', async () => { + mockCount.mockResolvedValue(0) + mockDeleteWhere + .mockResolvedValueOnce([]) // child-spot delete succeeds + .mockRejectedValueOnce(new Error('DB error')) // spot delete fails await expect(deleteSpot(makeDeleteSpotFormData())).rejects.toThrow( ERROR_MESSAGES.SPOT_DELETE_ERROR, @@ -369,29 +389,31 @@ describe('createMultipleSpots', () => { it('creates a room and beds inside it', async () => { const mockRoom = { id: 'room-1' } - ;(mockPrisma.placementSpot.create as jest.Mock) - .mockResolvedValueOnce(mockRoom) // room creation - .mockResolvedValueOnce({ id: 'bed-1' }) // bed 1 - .mockResolvedValueOnce({ id: 'bed-2' }) // bed 2 - .mockResolvedValueOnce({ id: 'bed-3' }) // bed 3 + mockInsertValues + .mockResolvedValueOnce([mockRoom]) // room creation + .mockResolvedValueOnce([{ id: 'bed-1' }]) // bed 1 + .mockResolvedValueOnce([{ id: 'bed-2' }]) // bed 2 + .mockResolvedValueOnce([{ id: 'bed-3' }]) // bed 3 await createMultipleSpots(makeMultipleSpotsFormData()) // First call: room - expect(mockPrisma.placementSpot.create).toHaveBeenNthCalledWith(1, { - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', code: 'R101', type: 'ROOM', status: 'AVAILABLE', }), - }) + ) // Subsequent calls: beds (3 beds) - expect(mockPrisma.placementSpot.create).toHaveBeenCalledTimes(4) // 1 room + 3 beds + expect(mockInsertValues).toHaveBeenCalledTimes(4) // 1 room + 3 beds - expect(mockPrisma.placementSpot.create).toHaveBeenNthCalledWith(2, { - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ housingUnitId: 'clxxxxxxxxxxxxxxxxx0001', code: 'R101-B1', label: 'Bett 1', @@ -399,27 +421,29 @@ describe('createMultipleSpots', () => { parentSpotId: 'room-1', status: 'AVAILABLE', }), - }) + ) - expect(mockPrisma.placementSpot.create).toHaveBeenNthCalledWith(3, { - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ code: 'R101-B2', label: 'Bett 2', parentSpotId: 'room-1', }), - }) + ) - expect(mockPrisma.placementSpot.create).toHaveBeenNthCalledWith(4, { - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenNthCalledWith( + 4, + expect.objectContaining({ code: 'R101-B3', label: 'Bett 3', parentSpotId: 'room-1', }), - }) + ) }) it('passes optional fields to room creation', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockResolvedValue({ id: 'room-1' }) + mockInsertValues.mockResolvedValue([{ id: 'room-1' }]) const fd = makeMultipleSpotsFormData({ roomLabel: 'Zimmer 101', @@ -430,27 +454,28 @@ describe('createMultipleSpots', () => { await createMultipleSpots(fd) - expect(mockPrisma.placementSpot.create).toHaveBeenNthCalledWith(1, { - data: expect.objectContaining({ + expect(mockInsertValues).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ label: 'Zimmer 101', squareMeters: 20, floor: 1, }), - }) + ) }) it('creates correct number of beds based on bedCount', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockResolvedValue({ id: 'room-1' }) + mockInsertValues.mockResolvedValue([{ id: 'room-1' }]) const fd = makeMultipleSpotsFormData({ bedCount: '1' }) await createMultipleSpots(fd) // 1 room + 1 bed = 2 total - expect(mockPrisma.placementSpot.create).toHaveBeenCalledTimes(2) + expect(mockInsertValues).toHaveBeenCalledTimes(2) }) - it('throws user-facing error when prisma fails', async () => { - ;(mockPrisma.placementSpot.create as jest.Mock).mockRejectedValue(new Error('DB error')) + it('throws user-facing error when the insert fails', async () => { + mockInsertValues.mockRejectedValue(new Error('DB error')) await expect(createMultipleSpots(makeMultipleSpotsFormData())).rejects.toThrow( ERROR_MESSAGES.SPOTS_BATCH_CREATE_ERROR, diff --git a/src/lib/actions/__tests__/transfers.test.ts b/src/lib/actions/__tests__/transfers.test.ts index 8a291fa8..66cd0e1d 100644 --- a/src/lib/actions/__tests__/transfers.test.ts +++ b/src/lib/actions/__tests__/transfers.test.ts @@ -6,7 +6,8 @@ * update status atomically, create audit log). */ -import { prisma } from '@/lib/db' +import { and, desc, eq } from 'drizzle-orm' +import { transferRequest } from '@/lib/db' import { logAudit } from '@/lib/audit' import { getTransferRequests, approveTransferRequest, denyTransferRequest } from '../transfers' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' @@ -15,13 +16,27 @@ import { ERROR_MESSAGES } from '@/lib/constants/error-messages' // MOCKS // ============================================================================= +const mockTransferFindMany = jest.fn() +const mockTransferFindFirst = jest.fn() +// Receives (setPayload, whereExpr) and resolves the updated-row array +const mockTransferUpdate = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - transferRequest: { - findMany: jest.fn(), - findUnique: jest.fn(), - updateMany: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + transferRequest: { + findMany: (...a: unknown[]) => mockTransferFindMany(...a), + findFirst: (...a: unknown[]) => mockTransferFindFirst(...a), + }, }, + update: jest.fn(() => ({ + set: (v: unknown) => ({ + where: (w: unknown) => ({ + returning: (): Promise => mockTransferUpdate(v, w), + }), + }), + })), }, })) @@ -64,8 +79,6 @@ jest.mock('@/lib/logger', () => ({ }, })) -const mockPrisma = prisma as jest.Mocked - beforeEach(() => { jest.clearAllMocks() }) @@ -80,43 +93,43 @@ describe('getTransferRequests', () => { { id: 'tr-1', status: 'PENDING', reason: 'Lärm' }, { id: 'tr-2', status: 'APPROVED', reason: 'Platzwechsel' }, ] - ;(mockPrisma.transferRequest.findMany as jest.Mock).mockResolvedValue(mockRequests) + mockTransferFindMany.mockResolvedValue(mockRequests) const result = await getTransferRequests() expect(result).toEqual(mockRequests) - expect(mockPrisma.transferRequest.findMany).toHaveBeenCalledWith({ - where: {}, - include: { + expect(mockTransferFindMany).toHaveBeenCalledWith({ + where: undefined, + with: { // displayName comes along so the staff queue can show a name rather // than a login code (RESIDENT_NAME_SELECT). - resident: { select: { id: true, code: true, displayName: true, supportLevel: true } }, + resident: { columns: { id: true, code: true, displayName: true, supportLevel: true } }, currentPlacement: { - select: { - id: true, - housingUnit: { select: { id: true, code: true, address: true } }, + columns: { id: true }, + with: { + housingUnit: { columns: { id: true, code: true, address: true } }, }, }, - targetUnit: { select: { id: true, code: true, address: true } }, + targetUnit: { columns: { id: true, code: true, address: true } }, }, - orderBy: { createdAt: 'desc' }, + orderBy: [desc(transferRequest.createdAt)], }) }) it('filters by status when provided', async () => { - ;(mockPrisma.transferRequest.findMany as jest.Mock).mockResolvedValue([]) + mockTransferFindMany.mockResolvedValue([]) await getTransferRequests('PENDING') - expect(mockPrisma.transferRequest.findMany).toHaveBeenCalledWith( + expect(mockTransferFindMany).toHaveBeenCalledWith( expect.objectContaining({ - where: { status: 'PENDING' }, + where: eq(transferRequest.status, 'PENDING'), }), ) }) it('returns empty array when no requests exist', async () => { - ;(mockPrisma.transferRequest.findMany as jest.Mock).mockResolvedValue([]) + mockTransferFindMany.mockResolvedValue([]) const result = await getTransferRequests() @@ -132,25 +145,25 @@ describe('approveTransferRequest', () => { const validInput = { requestId: 'clxxxxxxxxxxxxxxxxx0001', staffNotes: 'Genehmigt' } it('approves a pending request atomically and returns success', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 1 }) + mockTransferUpdate.mockResolvedValue([{ id: 'updated' }]) const result = await approveTransferRequest(validInput) expect(result).toEqual({ success: true }) - expect(mockPrisma.transferRequest.updateMany).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0001', status: 'PENDING' }, - data: { + expect(mockTransferUpdate).toHaveBeenCalledWith( + { status: 'APPROVED', staffNotes: 'Genehmigt', reviewedBy: 'staff-1', reviewedAt: expect.any(Date), }, - }) + and(eq(transferRequest.id, 'clxxxxxxxxxxxxxxxxx0001'), eq(transferRequest.status, 'PENDING')), + ) }) it('calls logAudit with userId on approval', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 1 }) + mockTransferUpdate.mockResolvedValue([{ id: 'updated' }]) await approveTransferRequest(validInput) @@ -164,8 +177,8 @@ describe('approveTransferRequest', () => { }) it('returns error when request is not found', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 0 }) - ;(mockPrisma.transferRequest.findUnique as jest.Mock).mockResolvedValue(null) + mockTransferUpdate.mockResolvedValue([]) + mockTransferFindFirst.mockResolvedValue(null) const result = await approveTransferRequest(validInput) @@ -174,8 +187,8 @@ describe('approveTransferRequest', () => { }) it('returns error when request has already been reviewed', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 0 }) - ;(mockPrisma.transferRequest.findUnique as jest.Mock).mockResolvedValue({ + mockTransferUpdate.mockResolvedValue([]) + mockTransferFindFirst.mockResolvedValue({ id: 'clxxxxxxxxxxxxxxxxx0001', }) @@ -186,7 +199,7 @@ describe('approveTransferRequest', () => { }) it('returns generic error when prisma fails', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockRejectedValue(new Error('DB error')) + mockTransferUpdate.mockRejectedValue(new Error('DB error')) const result = await approveTransferRequest(validInput) @@ -202,25 +215,25 @@ describe('denyTransferRequest', () => { const validInput = { requestId: 'clxxxxxxxxxxxxxxxxx0002', staffNotes: 'Kein Platz verfügbar' } it('denies a pending request atomically and returns success', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 1 }) + mockTransferUpdate.mockResolvedValue([{ id: 'updated' }]) const result = await denyTransferRequest(validInput) expect(result).toEqual({ success: true }) - expect(mockPrisma.transferRequest.updateMany).toHaveBeenCalledWith({ - where: { id: 'clxxxxxxxxxxxxxxxxx0002', status: 'PENDING' }, - data: { + expect(mockTransferUpdate).toHaveBeenCalledWith( + { status: 'DENIED', staffNotes: 'Kein Platz verfügbar', reviewedBy: 'staff-1', reviewedAt: expect.any(Date), }, - }) + and(eq(transferRequest.id, 'clxxxxxxxxxxxxxxxxx0002'), eq(transferRequest.status, 'PENDING')), + ) }) it('calls logAudit with userId on denial', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 1 }) + mockTransferUpdate.mockResolvedValue([{ id: 'updated' }]) await denyTransferRequest(validInput) @@ -234,8 +247,8 @@ describe('denyTransferRequest', () => { }) it('returns error when request is not found', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 0 }) - ;(mockPrisma.transferRequest.findUnique as jest.Mock).mockResolvedValue(null) + mockTransferUpdate.mockResolvedValue([]) + mockTransferFindFirst.mockResolvedValue(null) const result = await denyTransferRequest(validInput) @@ -244,8 +257,8 @@ describe('denyTransferRequest', () => { }) it('returns error when request has already been reviewed', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockResolvedValue({ count: 0 }) - ;(mockPrisma.transferRequest.findUnique as jest.Mock).mockResolvedValue({ + mockTransferUpdate.mockResolvedValue([]) + mockTransferFindFirst.mockResolvedValue({ id: 'clxxxxxxxxxxxxxxxxx0002', }) @@ -256,7 +269,7 @@ describe('denyTransferRequest', () => { }) it('returns generic error when prisma fails', async () => { - ;(mockPrisma.transferRequest.updateMany as jest.Mock).mockRejectedValue(new Error('DB error')) + mockTransferUpdate.mockRejectedValue(new Error('DB error')) const result = await denyTransferRequest(validInput) diff --git a/src/lib/analytics/__tests__/algorithm-accuracy.test.ts b/src/lib/analytics/__tests__/algorithm-accuracy.test.ts index 0a1b4845..d800d29a 100644 --- a/src/lib/analytics/__tests__/algorithm-accuracy.test.ts +++ b/src/lib/analytics/__tests__/algorithm-accuracy.test.ts @@ -15,9 +15,12 @@ const mockPlacementFindMany = jest.fn() const mockIncidentFindMany = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - placement: { findMany: (...args: unknown[]) => mockPlacementFindMany(...args) }, - incident: { findMany: (...args: unknown[]) => mockIncidentFindMany(...args) }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + placement: { findMany: (...args: unknown[]) => mockPlacementFindMany(...args) }, + incident: { findMany: (...args: unknown[]) => mockIncidentFindMany(...args) }, + }, }, })) diff --git a/src/lib/analytics/__tests__/mission-kpis.test.ts b/src/lib/analytics/__tests__/mission-kpis.test.ts index eb4f0da2..779504e9 100644 --- a/src/lib/analytics/__tests__/mission-kpis.test.ts +++ b/src/lib/analytics/__tests__/mission-kpis.test.ts @@ -16,13 +16,21 @@ const mockPlacementFindMany = jest.fn() const mockResidentFindMany = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - incident: { findMany: (...args: unknown[]) => mockIncidentFindMany(...args) }, - placement: { findMany: (...args: unknown[]) => mockPlacementFindMany(...args) }, - resident: { findMany: (...args: unknown[]) => mockResidentFindMany(...args) }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + incident: { findMany: (...args: unknown[]) => mockIncidentFindMany(...args) }, + placement: { findMany: (...args: unknown[]) => mockPlacementFindMany(...args) }, + resident: { findMany: (...args: unknown[]) => mockResidentFindMany(...args) }, + }, }, })) +// The source makes TWO placement queries (ended vs recent). They used to be +// told apart by the plain `where` object's keys; with drizzle the where is an +// SQL tree, so the dispatch reads the compared column back out of it. +import { whereParts } from '@/test-utils/drizzle-where' + // ============================================================================= // DATE FIXTURE // Fixed "now" = 2026-05-15 so month math is deterministic. @@ -136,19 +144,17 @@ describe('calculateMissionKPIs', () => { // ── Conflict relocation counting ────────────────────────────────────────── test('counts only CONFLICT end reason as relocations', async () => { - mockPlacementFindMany.mockImplementation( - (args: { where?: { endDate?: unknown; status?: unknown } }) => { - if (args.where?.endDate !== undefined) { - // Ended placements query - return Promise.resolve([ - { endDate: d('2026-04-10'), endReason: 'CONFLICT' }, - { endDate: d('2026-04-15'), endReason: 'NATURAL' }, // not a conflict relocation - { endDate: d('2026-04-20'), endReason: 'CONFLICT' }, - ]) - } - return Promise.resolve([]) // recent placements query - }, - ) + mockPlacementFindMany.mockImplementation((args: { where?: unknown }) => { + if ('endDate' in whereParts(args.where)) { + // Ended placements query + return Promise.resolve([ + { endDate: d('2026-04-10'), endReason: 'CONFLICT' }, + { endDate: d('2026-04-15'), endReason: 'NATURAL' }, // not a conflict relocation + { endDate: d('2026-04-20'), endReason: 'CONFLICT' }, + ]) + } + return Promise.resolve([]) // recent placements query + }) const kpis = await calculateMissionKPIs(6) const april = kpis.conflictRelocationsPerMonth.find((d) => d.month === '2026-04') @@ -157,14 +163,12 @@ describe('calculateMissionKPIs', () => { }) test('currentMonthRelocations counts May 2026 conflict ends', async () => { - mockPlacementFindMany.mockImplementation( - (args: { where?: { endDate?: unknown; status?: unknown } }) => { - if (args.where?.endDate !== undefined) { - return Promise.resolve([{ endDate: d('2026-05-05'), endReason: 'CONFLICT' }]) - } - return Promise.resolve([]) - }, - ) + mockPlacementFindMany.mockImplementation((args: { where?: unknown }) => { + if ('endDate' in whereParts(args.where)) { + return Promise.resolve([{ endDate: d('2026-05-05'), endReason: 'CONFLICT' }]) + } + return Promise.resolve([]) + }) const kpis = await calculateMissionKPIs(6) @@ -215,15 +219,13 @@ describe('calculateMissionKPIs', () => { mockResidentFindMany.mockResolvedValue([{ id: 'res-1', createdAt: residentCreatedAt }]) - mockPlacementFindMany.mockImplementation( - (args: { where?: { endDate?: unknown; startDate?: unknown } }) => { - if (args.where?.startDate !== undefined) { - // Recent placements - return Promise.resolve([{ residentId: 'res-1', startDate: firstPlacementDate }]) - } - return Promise.resolve([]) // ended placements - }, - ) + mockPlacementFindMany.mockImplementation((args: { where?: unknown }) => { + if ('startDate' in whereParts(args.where)) { + // Recent placements + return Promise.resolve([{ residentId: 'res-1', startDate: firstPlacementDate }]) + } + return Promise.resolve([]) // ended placements + }) const kpis = await calculateMissionKPIs(6) @@ -235,17 +237,15 @@ describe('calculateMissionKPIs', () => { mockResidentFindMany.mockResolvedValue([{ id: 'res-1', createdAt: residentCreatedAt }]) - mockPlacementFindMany.mockImplementation( - (args: { where?: { endDate?: unknown; startDate?: unknown } }) => { - if (args.where?.startDate !== undefined) { - return Promise.resolve([ - { residentId: 'res-1', startDate: d('2026-04-10T00:00:00Z') }, // 9 days - { residentId: 'res-1', startDate: d('2026-04-05T00:00:00Z') }, // 4 days ← earliest - ]) - } - return Promise.resolve([]) - }, - ) + mockPlacementFindMany.mockImplementation((args: { where?: unknown }) => { + if ('startDate' in whereParts(args.where)) { + return Promise.resolve([ + { residentId: 'res-1', startDate: d('2026-04-10T00:00:00Z') }, // 9 days + { residentId: 'res-1', startDate: d('2026-04-05T00:00:00Z') }, // 4 days ← earliest + ]) + } + return Promise.resolve([]) + }) const kpis = await calculateMissionKPIs(6) diff --git a/src/lib/analytics/__tests__/unit-metrics.test.ts b/src/lib/analytics/__tests__/unit-metrics.test.ts index 42cfe05c..ad571f95 100644 --- a/src/lib/analytics/__tests__/unit-metrics.test.ts +++ b/src/lib/analytics/__tests__/unit-metrics.test.ts @@ -2,10 +2,11 @@ * Unit tests for unit-metrics analytics * * Tests calculateUnitMetrics, calculateAllUnitMetrics, and - * getSimilarPlacementSuccessRate with Prisma mocked. + * getSimilarPlacementSuccessRate with the db module mocked. */ -import { prisma } from '@/lib/db' +import { and, gte, inArray, isNotNull, lte } from 'drizzle-orm' +import { housingUnit, placement } from '@/lib/db' import { calculateUnitMetrics, calculateAllUnitMetrics, @@ -16,24 +17,40 @@ import { // MOCKS // ============================================================================= -jest.mock('@/lib/db', () => ({ - prisma: { - housingUnit: { - findUnique: jest.fn(), - findMany: jest.fn(), - }, - incident: { - count: jest.fn(), - findMany: jest.fn().mockResolvedValue([]), - }, - placement: { - count: jest.fn(), - findMany: jest.fn(), +const mockHousingUnitFindFirst = jest.fn() +const mockHousingUnitFindMany = jest.fn() +const mockIncidentCount = jest.fn() +const mockIncidentFindMany = jest.fn().mockResolvedValue([]) +const mockPlacementCount = jest.fn() +const mockPlacementFindMany = jest.fn() + +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + return { + ...actual, + db: { + query: { + housingUnit: { + findFirst: (...a: unknown[]) => mockHousingUnitFindFirst(...a), + findMany: (...a: unknown[]) => mockHousingUnitFindMany(...a), + }, + incident: { findMany: (...a: unknown[]) => mockIncidentFindMany(...a) }, + placement: { findMany: (...a: unknown[]) => mockPlacementFindMany(...a) }, + }, + // `db.$count(table, where)` — dispatched on the table object, so the + // totalConflicts and activePlacements counts stay separately primeable. + $count: (table: unknown, where?: unknown) => + table === actual.incident ? mockIncidentCount(where) : mockPlacementCount(where), }, - }, -})) + } +}) -const mockPrisma = prisma as jest.Mocked +/** Same accessor shape the Prisma-era test used, so the test bodies read unchanged. */ +const mockDb = { + housingUnit: { findUnique: mockHousingUnitFindFirst, findMany: mockHousingUnitFindMany }, + incident: { count: mockIncidentCount, findMany: mockIncidentFindMany }, + placement: { count: mockPlacementCount, findMany: mockPlacementFindMany }, +} // ============================================================================= // FIXED TIME @@ -93,9 +110,9 @@ function makeIncident(date: string, overrides: Record = {}) { */ function setupDefaultMocks(unitOverrides: Record = {}) { const unit = makeUnit(unitOverrides) - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue(unit) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.housingUnit.findUnique as jest.Mock).mockResolvedValue(unit) + ;(mockDb.incident.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(0) return unit } @@ -109,7 +126,7 @@ describe('calculateUnitMetrics', () => { // --------------------------------------------------------------------------- it('throws when unit is not found', async () => { - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue(null) + ;(mockDb.housingUnit.findUnique as jest.Mock).mockResolvedValue(null) await expect(calculateUnitMetrics('nonexistent')).rejects.toThrow('Unit nonexistent not found') }) @@ -209,7 +226,7 @@ describe('calculateUnitMetrics', () => { it('returns totalConflicts from prisma.incident.count', async () => { setupDefaultMocks() // First call is totalConflicts, subsequent calls are incident-free month checks - ;(mockPrisma.incident.count as jest.Mock) + ;(mockDb.incident.count as jest.Mock) .mockResolvedValueOnce(42) // totalConflicts .mockResolvedValue(0) // incident-free months loop @@ -429,7 +446,7 @@ describe('calculateUnitMetrics', () => { describe('occupancy', () => { it('calculates occupancy rate from active placements and total beds', async () => { setupDefaultMocks({ totalBeds: 4 }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(3) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(3) const result = await calculateUnitMetrics('unit-1') @@ -439,7 +456,7 @@ describe('calculateUnitMetrics', () => { it('returns 100 when fully occupied', async () => { setupDefaultMocks({ totalBeds: 2 }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(2) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(2) const result = await calculateUnitMetrics('unit-1') @@ -448,7 +465,7 @@ describe('calculateUnitMetrics', () => { it('returns 0 when empty', async () => { setupDefaultMocks({ totalBeds: 4 }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(0) const result = await calculateUnitMetrics('unit-1') @@ -538,7 +555,7 @@ describe('calculateUnitMetrics', () => { it('returns 12 when all months are incident-free', async () => { setupDefaultMocks() // totalConflicts = 0, then 12 months of 0 incidents - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.incident.count as jest.Mock).mockResolvedValue(0) const result = await calculateUnitMetrics('unit-1') @@ -549,7 +566,7 @@ describe('calculateUnitMetrics', () => { setupDefaultMocks() // Refactor: incidentFreeMonths now derives from a single findMany over // the last 12 months; the JS loop buckets dates by Zurich month. - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-07-10'), // July 2025: current month has an incident ]) @@ -559,7 +576,7 @@ describe('calculateUnitMetrics', () => { it('counts consecutive months backward until an incident is found', async () => { setupDefaultMocks() - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-04-15'), // April: incident, break after counting 3 free ]) const result = await calculateUnitMetrics('unit-1') @@ -568,7 +585,7 @@ describe('calculateUnitMetrics', () => { it('stops counting at the first month with incidents', async () => { setupDefaultMocks() - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-06-15'), // June: incident -> only July counted makeIncident('2025-05-10'), // (May also has incidents but we already broke) ]) @@ -668,7 +685,7 @@ describe('calculateUnitMetrics', () => { it('returns "Sehr stabil" when incidentFreeMonths >= 6', async () => { setupDefaultMocks() // totalConflicts = 0, all 12 months free - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.incident.count as jest.Mock).mockResolvedValue(0) const result = await calculateUnitMetrics('unit-1') @@ -685,8 +702,8 @@ describe('calculateUnitMetrics', () => { */ it('returns "Stabil" when incidentFreeMonths is 3-5', async () => { setupDefaultMocks() - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(5) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(5) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-04-15'), // April incident -> 3 free months (Jul, Jun, May) ]) @@ -712,8 +729,8 @@ describe('calculateUnitMetrics', () => { makeIncident('2025-05-20'), // 30-60 day window (before June 15, after May 16) ] setupDefaultMocks({ incidents }) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(5) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(5) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-07-10'), // current month -> incidentFreeMonths=0 ]) @@ -739,8 +756,8 @@ describe('calculateUnitMetrics', () => { makeIncident('2025-05-25'), // 30-60 day window ] setupDefaultMocks({ incidents }) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(10) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(10) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-07-10'), // current month -> incidentFreeMonths=0 ]) @@ -763,10 +780,8 @@ describe('calculateUnitMetrics', () => { makeIncident('2025-02-10'), ] setupDefaultMocks({ incidents }) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(20) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ - makeIncident('2025-07-10'), - ]) + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(20) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([makeIncident('2025-07-10')]) const result = await calculateUnitMetrics('unit-1') @@ -781,10 +796,8 @@ describe('calculateUnitMetrics', () => { // incidentFreeMonths = 0 (because current month has incidents via count mock) const incidents = [makeIncident('2025-04-01')] setupDefaultMocks({ incidents }) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(1) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ - makeIncident('2025-07-10'), - ]) + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(1) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([makeIncident('2025-07-10')]) const result = await calculateUnitMetrics('unit-1') @@ -802,7 +815,7 @@ describe('calculateUnitMetrics', () => { describe('rounding', () => { it('rounds occupancyRate to integer', async () => { setupDefaultMocks({ totalBeds: 3 }) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(1) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(1) const result = await calculateUnitMetrics('unit-1') @@ -856,14 +869,14 @@ describe('calculateUnitMetrics', () => { makeIncident('2025-06-01'), // not recent ] - ;(mockPrisma.housingUnit.findUnique as jest.Mock).mockResolvedValue( + ;(mockDb.housingUnit.findUnique as jest.Mock).mockResolvedValue( makeUnit({ placements, incidents, totalBeds: 3 }), ) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValueOnce(8) // totalConflicts - ;(mockPrisma.incident.findMany as jest.Mock).mockResolvedValueOnce([ + ;(mockDb.incident.count as jest.Mock).mockResolvedValueOnce(8) // totalConflicts + ;(mockDb.incident.findMany as jest.Mock).mockResolvedValueOnce([ makeIncident('2025-07-10'), // July: incident -> incidentFreeMonths=0 ]) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(1) // 1 active + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(1) // 1 active const result = await calculateUnitMetrics('unit-1') @@ -888,7 +901,7 @@ describe('calculateUnitMetrics', () => { describe('calculateAllUnitMetrics', () => { it('returns empty array when no units match', async () => { - ;(mockPrisma.housingUnit.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.housingUnit.findMany as jest.Mock).mockResolvedValue([]) const result = await calculateAllUnitMetrics() @@ -896,7 +909,7 @@ describe('calculateAllUnitMetrics', () => { }) it('calls calculateUnitMetrics for each unit with AVAILABLE or FULL status', async () => { - ;(mockPrisma.housingUnit.findMany as jest.Mock).mockResolvedValue([ + ;(mockDb.housingUnit.findMany as jest.Mock).mockResolvedValue([ { id: 'unit-a' }, { id: 'unit-b' }, ]) @@ -905,11 +918,11 @@ describe('calculateAllUnitMetrics', () => { const unitA = makeUnit({ id: 'unit-a', code: 'WG-A' }) const unitB = makeUnit({ id: 'unit-b', code: 'WG-B' }) - ;(mockPrisma.housingUnit.findUnique as jest.Mock) + ;(mockDb.housingUnit.findUnique as jest.Mock) .mockResolvedValueOnce(unitA) .mockResolvedValueOnce(unitB) - ;(mockPrisma.incident.count as jest.Mock).mockResolvedValue(0) - ;(mockPrisma.placement.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.incident.count as jest.Mock).mockResolvedValue(0) + ;(mockDb.placement.count as jest.Mock).mockResolvedValue(0) const result = await calculateAllUnitMetrics() @@ -919,13 +932,15 @@ describe('calculateAllUnitMetrics', () => { }) it('queries units with status AVAILABLE or FULL', async () => { - ;(mockPrisma.housingUnit.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.housingUnit.findMany as jest.Mock).mockResolvedValue([]) await calculateAllUnitMetrics() - expect(mockPrisma.housingUnit.findMany).toHaveBeenCalledWith({ - where: { status: { in: ['AVAILABLE', 'FULL'] } }, - select: { id: true }, + // Compared against the REAL drizzle expression — same column, operator + // and values, without asserting on the SQL tree's internals by hand. + expect(mockDb.housingUnit.findMany).toHaveBeenCalledWith({ + where: inArray(housingUnit.status, ['AVAILABLE', 'FULL']), + columns: { id: true }, }) }) }) @@ -936,7 +951,7 @@ describe('calculateAllUnitMetrics', () => { describe('getSimilarPlacementSuccessRate', () => { it('returns zeros when no placements found', async () => { - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue([]) const result = await getSimilarPlacementSuccessRate(75) @@ -948,34 +963,30 @@ describe('getSimilarPlacementSuccessRate', () => { }) it('queries placements within score +/- range', async () => { - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue([]) await getSimilarPlacementSuccessRate(75, 15) - expect(mockPrisma.placement.findMany).toHaveBeenCalledWith({ - where: { - compatibilityScore: { - gte: 60, // 75 - 15 - lte: 90, // 75 + 15 - }, - endDate: { not: null }, - }, + expect(mockDb.placement.findMany).toHaveBeenCalledWith({ + where: and( + gte(placement.compatibilityScore, 60), // 75 - 15 + lte(placement.compatibilityScore, 90), // 75 + 15 + isNotNull(placement.endDate), + ), }) }) it('uses default range of 10', async () => { - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue([]) await getSimilarPlacementSuccessRate(80) - expect(mockPrisma.placement.findMany).toHaveBeenCalledWith({ - where: { - compatibilityScore: { - gte: 70, // 80 - 10 - lte: 90, // 80 + 10 - }, - endDate: { not: null }, - }, + expect(mockDb.placement.findMany).toHaveBeenCalledWith({ + where: and( + gte(placement.compatibilityScore, 70), // 80 - 10 + lte(placement.compatibilityScore, 90), // 80 + 10 + isNotNull(placement.endDate), + ), }) }) @@ -1000,7 +1011,7 @@ describe('getSimilarPlacementSuccessRate', () => { endReason: 'COMPLETED', }, ] - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue(placements) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue(placements) const result = await getSimilarPlacementSuccessRate(75) @@ -1026,7 +1037,7 @@ describe('getSimilarPlacementSuccessRate', () => { endReason: 'REQUEST', // not CONFLICT, so still successful }, ] - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue(placements) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue(placements) const result = await getSimilarPlacementSuccessRate(75) @@ -1052,7 +1063,7 @@ describe('getSimilarPlacementSuccessRate', () => { endReason: 'CONFLICT', }, ] - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue(placements) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue(placements) const result = await getSimilarPlacementSuccessRate(75) @@ -1084,7 +1095,7 @@ describe('getSimilarPlacementSuccessRate', () => { endReason: 'COMPLETED', }, ] - ;(mockPrisma.placement.findMany as jest.Mock).mockResolvedValue(placements) + ;(mockDb.placement.findMany as jest.Mock).mockResolvedValue(placements) const result = await getSimilarPlacementSuccessRate(75) diff --git a/src/lib/auth/__tests__/account.test.ts b/src/lib/auth/__tests__/account.test.ts index 9428b20c..6dc4526a 100644 --- a/src/lib/auth/__tests__/account.test.ts +++ b/src/lib/auth/__tests__/account.test.ts @@ -5,18 +5,57 @@ * guarantees. */ -const mockPrisma = { - user: { findUnique: jest.fn(), update: jest.fn() }, - resident: { findUnique: jest.fn() }, - account: { - findFirst: jest.fn(), - findUnique: jest.fn(), - findUniqueOrThrow: jest.fn(), - create: jest.fn(), - update: jest.fn(), - }, +/** + * Pull `column = value` out of a drizzle eq() expression, so mocks can + * dispatch the way the old tests dispatched on Prisma's `where` objects. + * All three Prisma account lookups (findFirst by identity, findUnique by + * email, findUniqueOrThrow by id) are now ONE db.query.account.findFirst — + * the where column is what tells them apart. + */ +function mockEqParts(where: unknown): { column?: string; value?: unknown } { + const parts: { column?: string; value?: unknown } = {} + for (const chunk of (where as { queryChunks?: unknown[] })?.queryChunks ?? []) { + if (chunk && typeof chunk === 'object') { + if ('name' in chunk && 'table' in chunk) parts.column = (chunk as { name: string }).name + else if ('encoder' in chunk) parts.value = (chunk as unknown as { value: unknown }).value + } + } + return parts } -jest.mock('@/lib/db', () => ({ prisma: mockPrisma })) + +const mockUserFindFirst = jest.fn() +const mockResidentFindFirst = jest.fn() +const mockAccountFindFirst = jest.fn() +// (table, values) => inserted rows for .returning() +const mockInsert = jest.fn() +// (table, data, { column, value }) => updated rows for .returning() +const mockUpdate = jest.fn() + +jest.mock('@/lib/db', () => ({ + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { findFirst: (...a: unknown[]) => mockUserFindFirst(...a) }, + resident: { findFirst: (...a: unknown[]) => mockResidentFindFirst(...a) }, + account: { findFirst: (...a: unknown[]) => mockAccountFindFirst(...a) }, + }, + insert: (table: unknown) => ({ + values: (v: unknown) => ({ + returning: () => Promise.resolve(mockInsert(table, v)), + }), + }), + update: (table: unknown) => ({ + set: (data: unknown) => ({ + where: (w: unknown) => { + const rows = mockUpdate(table, data, mockEqParts(w)) + return Object.assign(Promise.resolve(rows), { + returning: () => Promise.resolve(rows), + }) + }, + }), + }), + }, +})) const mockSendEmail = jest.fn() jest.mock('@/lib/email/service', () => ({ @@ -40,6 +79,7 @@ jest.mock('@/lib/logger', () => ({ import { registerAccount, loginWithEmail, requestPasswordReset, resetPassword } from '../account' import { hashPassword } from '../passwords' import { ERROR_MESSAGES } from '@/lib/constants/error-messages' +import { account as accountTable, user as userTable } from '@/lib/db' const STAFF = { id: 'user-1', @@ -83,13 +123,23 @@ function account( } } +// The three account lookups the source makes, told apart by their where +// column. These replace the old findFirst/findUnique/findUniqueOrThrow trio. +const mockAccountByIdentity = jest.fn() // eq(account.userId | residentId, …) +const mockAccountByEmail = jest.fn() // eq(account.email, …) +const mockAccountById = jest.fn() // eq(account.id, …) — loadAccount + /** Code lookups resolve; nothing else exists unless a test says so. */ function codeExists({ staff = false, resident = false } = {}) { - mockPrisma.user.findUnique.mockImplementation(({ where }: { where: { code?: string } }) => - Promise.resolve(staff && where.code === STAFF.code ? { id: STAFF.id, active: true } : null), + mockUserFindFirst.mockImplementation(({ where }: { where: unknown }) => + Promise.resolve( + staff && mockEqParts(where).value === STAFF.code ? { id: STAFF.id, active: true } : null, + ), ) - mockPrisma.resident.findUnique.mockImplementation(({ where }: { where: { code?: string } }) => - Promise.resolve(resident && where.code === RESIDENT.code ? { id: RESIDENT.id } : null), + mockResidentFindFirst.mockImplementation(({ where }: { where: unknown }) => + Promise.resolve( + resident && mockEqParts(where).value === RESIDENT.code ? { id: RESIDENT.id } : null, + ), ) } @@ -97,12 +147,17 @@ beforeEach(() => { jest.clearAllMocks() mockEmailConfig.enabled = true codeExists() - mockPrisma.user.update.mockResolvedValue({}) - mockPrisma.account.findFirst.mockResolvedValue(null) - mockPrisma.account.findUnique.mockResolvedValue(null) - mockPrisma.account.create.mockResolvedValue({ id: 'acc-new' }) - mockPrisma.account.update.mockResolvedValue({ id: 'acc-1' }) - mockPrisma.account.findUniqueOrThrow.mockResolvedValue(account()) + mockAccountFindFirst.mockImplementation(({ where }: { where: unknown }) => { + const { column, value } = mockEqParts(where) + if (column === 'email') return mockAccountByEmail(value) + if (column === 'id') return mockAccountById(value) + return mockAccountByIdentity(column, value) + }) + mockAccountByIdentity.mockResolvedValue(null) + mockAccountByEmail.mockResolvedValue(null) + mockAccountById.mockResolvedValue(account()) + mockInsert.mockReturnValue([{ id: 'acc-new' }]) + mockUpdate.mockReturnValue([{ id: 'acc-1' }]) mockCreateAuthToken.mockResolvedValue('raw-token') mockSendEmail.mockResolvedValue(true) }) @@ -110,7 +165,7 @@ beforeEach(() => { describe('registerAccount', () => { it('claims a staff code: creates the account with a bcrypt hash, sends verification', async () => { codeExists({ staff: true }) - mockPrisma.account.findUniqueOrThrow.mockResolvedValue(account({ id: 'acc-new', user: STAFF })) + mockAccountById.mockResolvedValue(account({ id: 'acc-new', user: STAFF })) const result = await registerAccount({ code: 'AOZ-ADMIN1', @@ -123,7 +178,8 @@ describe('registerAccount', () => { expect(result.identities.staff).toMatchObject({ code: 'AOZ-ADMIN1', email: 'g@example.ch' }) expect(result.identities.resident).toBeUndefined() - const created = mockPrisma.account.create.mock.calls[0][0].data + const [table, created] = mockInsert.mock.calls[0] + expect(table).toBe(accountTable) expect(created).toMatchObject({ email: 'g@example.ch', userId: 'user-1' }) expect(created.passwordHash).toMatch(/^\$2[aby]\$/) expect(created.passwordHash).not.toBe('secret-password') @@ -134,7 +190,7 @@ describe('registerAccount', () => { it('claims a resident code and returns the resident identity', async () => { codeExists({ resident: true }) - mockPrisma.account.findUniqueOrThrow.mockResolvedValue( + mockAccountById.mockResolvedValue( account({ id: 'acc-new', email: 'ihor@example.ch', resident: RESIDENT }), ) @@ -147,7 +203,7 @@ describe('registerAccount', () => { expect(result.success).toBe(true) if (!result.success) return expect(result.identities.resident).toEqual({ id: 'res-1', code: 'RES-ABC123' }) - expect(mockPrisma.account.create.mock.calls[0][0].data).toMatchObject({ residentId: 'res-1' }) + expect(mockInsert.mock.calls[0][1]).toMatchObject({ residentId: 'res-1' }) }) it('rejects an unknown code', async () => { @@ -160,7 +216,7 @@ describe('registerAccount', () => { }) it('rejects a DEACTIVATED staff code with the same message as an unknown one', async () => { - mockPrisma.user.findUnique.mockResolvedValue({ id: STAFF.id, active: false }) + mockUserFindFirst.mockResolvedValue({ id: STAFF.id, active: false }) const result = await registerAccount({ code: 'AOZ-ADMIN1', email: 'x@example.ch', @@ -171,7 +227,7 @@ describe('registerAccount', () => { it('rejects a code that already carries credentials (use password reset)', async () => { codeExists({ resident: true }) - mockPrisma.account.findFirst.mockResolvedValue( + mockAccountByIdentity.mockResolvedValue( account({ passwordHash: 'already-set', resident: RESIDENT }), ) const result = await registerAccount({ @@ -184,8 +240,8 @@ describe('registerAccount', () => { it('rejects an email owned by a DIFFERENT account than the code is on', async () => { codeExists({ resident: true }) - mockPrisma.account.findFirst.mockResolvedValue(account({ id: 'acc-res', resident: RESIDENT })) - mockPrisma.account.findUnique.mockResolvedValue(account({ id: 'acc-other' })) + mockAccountByIdentity.mockResolvedValue(account({ id: 'acc-res', resident: RESIDENT })) + mockAccountByEmail.mockResolvedValue(account({ id: 'acc-other' })) const result = await registerAccount({ code: 'RES-ABC123', @@ -198,9 +254,9 @@ describe('registerAccount', () => { it('completes an unclaimed account that already knows the email (invited staff)', async () => { codeExists({ staff: true }) const unclaimed = account({ user: STAFF }) - mockPrisma.account.findFirst.mockResolvedValue(unclaimed) - mockPrisma.account.findUnique.mockResolvedValue(unclaimed) - mockPrisma.account.findUniqueOrThrow.mockResolvedValue(account({ user: STAFF })) + mockAccountByIdentity.mockResolvedValue(unclaimed) + mockAccountByEmail.mockResolvedValue(unclaimed) + mockAccountById.mockResolvedValue(account({ user: STAFF })) const result = await registerAccount({ code: 'AOZ-ADMIN1', @@ -209,8 +265,8 @@ describe('registerAccount', () => { }) expect(result.success).toBe(true) - expect(mockPrisma.account.create).not.toHaveBeenCalled() - expect(mockPrisma.account.update.mock.calls[0][0].where).toEqual({ id: 'acc-1' }) + expect(mockInsert).not.toHaveBeenCalled() + expect(mockUpdate.mock.calls[0][2]).toEqual({ column: 'id', value: 'acc-1' }) }) it('still succeeds when the verification email fails to send', async () => { @@ -231,9 +287,9 @@ describe('registerAccount — linking a SECOND role to one login', () => { async function linkResidentToStaffAccount(password: string) { codeExists({ resident: true }) const staffAccount = account({ id: 'acc-staff', passwordHash: ACCOUNT_HASH, user: STAFF }) - mockPrisma.account.findFirst.mockResolvedValue(null) - mockPrisma.account.findUnique.mockResolvedValue(staffAccount) - mockPrisma.account.findUniqueOrThrow.mockResolvedValue({ + mockAccountByIdentity.mockResolvedValue(null) + mockAccountByEmail.mockResolvedValue(staffAccount) + mockAccountById.mockResolvedValue({ ...staffAccount, resident: RESIDENT, }) @@ -249,12 +305,13 @@ describe('registerAccount — linking a SECOND role to one login', () => { expect(result.identities.staff).toMatchObject({ code: 'AOZ-ADMIN1' }) expect(result.identities.resident).toEqual({ id: 'res-1', code: 'RES-ABC123' }) - expect(mockPrisma.account.update).toHaveBeenCalledWith({ - where: { id: 'acc-staff' }, - data: { residentId: 'res-1' }, - }) + expect(mockUpdate).toHaveBeenCalledWith( + accountTable, + { residentId: 'res-1' }, + { column: 'id', value: 'acc-staff' }, + ) // Linking must not touch the existing password. - expect(mockPrisma.account.update.mock.calls[0][0].data.passwordHash).toBeUndefined() + expect(mockUpdate.mock.calls[0][1].passwordHash).toBeUndefined() }) it('refuses to link without the account password (a stray code is not enough)', async () => { @@ -263,12 +320,12 @@ describe('registerAccount — linking a SECOND role to one login', () => { success: false, error: ERROR_MESSAGES.AUTH_LINK_PASSWORD_MISMATCH, }) - expect(mockPrisma.account.update).not.toHaveBeenCalled() + expect(mockUpdate).not.toHaveBeenCalled() }) it('refuses a second code for a role slot that is already filled', async () => { codeExists({ resident: true }) - mockPrisma.account.findUnique.mockResolvedValue( + mockAccountByEmail.mockResolvedValue( account({ id: 'acc-other', passwordHash: 'x', @@ -287,7 +344,7 @@ describe('registerAccount — linking a SECOND role to one login', () => { describe('loginWithEmail', () => { it('returns EVERY identity the account carries', async () => { - mockPrisma.account.findUnique.mockResolvedValue( + mockAccountByEmail.mockResolvedValue( account({ passwordHash: ACCOUNT_HASH, user: STAFF, @@ -300,14 +357,15 @@ describe('loginWithEmail', () => { if (!result.success) return expect(result.identities.staff).toMatchObject({ id: 'user-1', role: 'ADMIN' }) expect(result.identities.resident).toEqual({ id: 'res-1', code: 'RES-ABC123' }) - expect(mockPrisma.user.update).toHaveBeenCalledWith({ - where: { id: 'user-1' }, - data: { lastLoginAt: expect.any(Date) }, - }) + expect(mockUpdate).toHaveBeenCalledWith( + userTable, + { lastLoginAt: expect.any(Date) }, + { column: 'id', value: 'user-1' }, + ) }) it('keeps resident access when the staff identity is deactivated', async () => { - mockPrisma.account.findUnique.mockResolvedValue( + mockAccountByEmail.mockResolvedValue( account({ passwordHash: ACCOUNT_HASH, user: { ...STAFF, active: false }, @@ -327,21 +385,19 @@ describe('loginWithEmail', () => { [ 'wrong password', async () => { - mockPrisma.account.findUnique.mockResolvedValue( - account({ passwordHash: ACCOUNT_HASH, user: STAFF }), - ) + mockAccountByEmail.mockResolvedValue(account({ passwordHash: ACCOUNT_HASH, user: STAFF })) }, ], [ 'account without a password', () => { - mockPrisma.account.findUnique.mockResolvedValue(account({ user: STAFF })) + mockAccountByEmail.mockResolvedValue(account({ user: STAFF })) }, ], [ 'every identity deactivated', async () => { - mockPrisma.account.findUnique.mockResolvedValue( + mockAccountByEmail.mockResolvedValue( account({ passwordHash: ACCOUNT_HASH, user: { ...STAFF, active: false } }), ) }, @@ -365,7 +421,7 @@ describe('requestPasswordReset', () => { }) it('sends a reset link to an existing account', async () => { - mockPrisma.account.findUnique.mockResolvedValue({ id: 'acc-1' }) + mockAccountByEmail.mockResolvedValue({ id: 'acc-1' }) const result = await requestPasswordReset('g@example.ch') expect(result).toEqual({ success: true }) expect(mockCreateAuthToken).toHaveBeenCalledWith('acc-1', 'RESET_PASSWORD') @@ -387,16 +443,17 @@ describe('resetPassword', () => { const result = await resetPassword('raw-token', 'new-password-123') expect(result).toEqual({ success: true }) - const update = mockPrisma.account.update.mock.calls[0][0] - expect(update.where).toEqual({ id: 'acc-1' }) - expect(update.data.passwordHash).toMatch(/^\$2[aby]\$/) - expect(update.data.emailVerifiedAt).toBeInstanceOf(Date) + const [table, data, where] = mockUpdate.mock.calls[0] + expect(table).toBe(accountTable) + expect(where).toEqual({ column: 'id', value: 'acc-1' }) + expect(data.passwordHash).toMatch(/^\$2[aby]\$/) + expect(data.emailVerifiedAt).toBeInstanceOf(Date) }) it('rejects an invalid or expired token', async () => { mockConsumeAuthToken.mockResolvedValue(null) const result = await resetPassword('raw-token', 'new-password-123') expect(result).toEqual({ success: false, error: ERROR_MESSAGES.AUTH_RESET_TOKEN_INVALID }) - expect(mockPrisma.account.update).not.toHaveBeenCalled() + expect(mockUpdate).not.toHaveBeenCalled() }) }) diff --git a/src/lib/auth/__tests__/admin-page-guards.test.ts b/src/lib/auth/__tests__/admin-page-guards.test.ts index f5d2a9e6..44bb9234 100644 --- a/src/lib/auth/__tests__/admin-page-guards.test.ts +++ b/src/lib/auth/__tests__/admin-page-guards.test.ts @@ -139,7 +139,7 @@ describe('the settings page does not ship anyone a credential', () => { */ it('does not select staff codes', () => { const source = fs.readFileSync(path.join(ADMIN_DIR, 'settings/page.tsx'), 'utf8') - const select = source.match(/prisma\.user\.findMany\(\{[\s\S]*?\n {4}\}\)/) + const select = source.match(/db\.query\.user\.findMany\(\{[\s\S]*?\n {4}\}\)/) expect(select).not.toBeNull() expect(select?.[0]).not.toMatch(/\bcode:\s*true\b/) @@ -200,7 +200,7 @@ describe('the analytics page does not bypass the placements boundary', () => { /canReadPlacements\s*=\s*hasPermission\(\s*currentUser,\s*'placements:read'\s*\)/, ) - const queryLine = source.match(/canReadPlacements\s*\n?\s*\?\s*prisma\.placement\.findMany/) + const queryLine = source.match(/canReadPlacements\s*\n?\s*\?\s*db\.query\.placement\.findMany/) expect(queryLine).not.toBeNull() const renderLine = source.match(/canReadPlacements\s*&&\s* { const source = fs.readFileSync(ROUTE, 'utf8') - expect(source).toMatch(/prisma\.complaint\.create/) - expect(source).not.toMatch(/prisma\.incident\.create/) - expect(source).not.toMatch(/prisma\.maintenanceRequest\.create/) + expect(source).toMatch(/\.insert\(complaint\)/) + expect(source).not.toMatch(/\.insert\(incident\)/) + expect(source).not.toMatch(/\.insert\(maintenanceRequest\)/) }) it('an anonymous complaint stores no resident, and no audit row names one', () => { diff --git a/src/lib/auth/__tests__/household-aoz-gate.test.ts b/src/lib/auth/__tests__/household-aoz-gate.test.ts index d0a7a03b..02a6cd94 100644 --- a/src/lib/auth/__tests__/household-aoz-gate.test.ts +++ b/src/lib/auth/__tests__/household-aoz-gate.test.ts @@ -12,22 +12,22 @@ * that mints identities into it must not exist — not "must be hard to reach". */ +const mockAccountFindFirst = jest.fn() +const mockTransaction = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - account: { findUnique: jest.fn() }, - $transaction: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + account: { findFirst: (...a: unknown[]) => mockAccountFindFirst(...a) }, + }, + transaction: (...a: unknown[]) => mockTransaction(...a), }, })) -import { prisma } from '@/lib/db' import { BRAND } from '@/lib/config/brand' import { registerWithNewHousehold } from '@/lib/auth/household' -const mockPrisma = prisma as unknown as { - account: { findUnique: jest.Mock } - $transaction: jest.Mock -} - describe('self-serve household on an AOZ deployment', () => { it('is off in the shipped AOZ configuration', () => { // If this ever flips, the assertions below stop meaning anything — so it @@ -48,7 +48,7 @@ describe('self-serve household on an AOZ deployment', () => { expect(result.success).toBe(false) // The point is not just the return value: nothing may be written, and the // refusal must land before any database work is attempted at all. - expect(mockPrisma.$transaction).not.toHaveBeenCalled() - expect(mockPrisma.account.findUnique).not.toHaveBeenCalled() + expect(mockTransaction).not.toHaveBeenCalled() + expect(mockAccountFindFirst).not.toHaveBeenCalled() }) }) diff --git a/src/lib/auth/__tests__/household.test.ts b/src/lib/auth/__tests__/household.test.ts index fcad02da..3aee80ed 100644 --- a/src/lib/auth/__tests__/household.test.ts +++ b/src/lib/auth/__tests__/household.test.ts @@ -8,10 +8,16 @@ * kind of property that a later refactor could quietly undo. */ +const mockAccountFindFirst = jest.fn() +const mockTransaction = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - account: { findUnique: jest.fn() }, - $transaction: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + account: { findFirst: (...a: unknown[]) => mockAccountFindFirst(...a) }, + }, + transaction: (fn: (tx: unknown) => unknown) => mockTransaction(fn), }, })) @@ -67,13 +73,14 @@ jest.mock('@/lib/auth/code-generation', () => ({ generateResidentCode: () => 'MB-TEST01', })) -import { prisma } from '@/lib/db' import { registerWithNewHousehold } from '@/lib/auth/household' - -const mockPrisma = prisma as unknown as { - account: { findUnique: jest.Mock } - $transaction: jest.Mock -} +import { + housingUnit, + resident as residentTable, + placement, + account as accountTable, + user as userTable, +} from '@/lib/db' /** Captures every table the transaction wrote, so we can assert on absence. */ function transactionSpy() { @@ -85,48 +92,41 @@ function transactionSpy() { user: [], } - const tx = { - housingUnit: { - create: jest.fn(async ({ data }: { data: unknown }) => { - created.housingUnit.push(data) - return { id: 'unit-1' } - }), - }, - resident: { - create: jest.fn(async ({ data }: { data: unknown }) => { - created.resident.push(data) - return { id: 'res-1', code: 'MB-TEST01' } - }), + const tableKeys = new Map([ + [housingUnit, 'housingUnit'], + [residentTable, 'resident'], + [placement, 'placement'], + [accountTable, 'account'], + // Present but must never be written to. + [userTable, 'user'], + ]) + const returningRows = new Map([ + [housingUnit, [{ id: 'unit-1' }]], + [residentTable, [{ id: 'res-1', code: 'MB-TEST01' }]], + [placement, [{ id: 'pl-1' }]], + [accountTable, [{ id: 'acc-1' }]], + [userTable, [{ id: 'user-1' }]], + ]) + + const insert = jest.fn((table: unknown) => ({ + values: (data: unknown) => { + created[tableKeys.get(table) ?? 'unknown']?.push(data) + // `.values()` alone is awaitable; `.returning()` yields the created row. + return Object.assign(Promise.resolve(), { + returning: () => Promise.resolve(returningRows.get(table)), + }) }, - placement: { - create: jest.fn(async ({ data }: { data: unknown }) => { - created.placement.push(data) - return { id: 'pl-1' } - }), - }, - account: { - create: jest.fn(async ({ data }: { data: unknown }) => { - created.account.push(data) - return { id: 'acc-1' } - }), - }, - // Present but must never be called. - user: { - create: jest.fn(async ({ data }: { data: unknown }) => { - created.user.push(data) - return { id: 'user-1' } - }), - }, - } + })) + const tx = { insert } - mockPrisma.$transaction.mockImplementation(async (fn: (t: typeof tx) => unknown) => fn(tx)) - return { tx, created } + mockTransaction.mockImplementation(async (fn: (t: typeof tx) => unknown) => fn(tx)) + return { tx, created, insert } } beforeEach(() => { jest.clearAllMocks() globalThis.__selfServeHousehold = true - mockPrisma.account.findUnique.mockResolvedValue(null) + mockAccountFindFirst.mockResolvedValue(null) }) const INPUT = { @@ -150,11 +150,11 @@ describe('registerWithNewHousehold', () => { }) it('NEVER creates a staff user — staff permissions are global', async () => { - const { tx, created } = transactionSpy() + const { created, insert } = transactionSpy() const result = await registerWithNewHousehold(INPUT) - expect(tx.user.create).not.toHaveBeenCalled() + expect(insert).not.toHaveBeenCalledWith(userTable) expect(created.user).toHaveLength(0) // And the session it asks for carries no staff side at all. expect(result.success && 'staff' in result.identities).toBe(false) @@ -197,13 +197,13 @@ describe('registerWithNewHousehold', () => { // safeguarding gate through a mock only proves the mock works. it('refuses an email that already has an account', async () => { - mockPrisma.account.findUnique.mockResolvedValue({ id: 'existing' }) - const { tx } = transactionSpy() + mockAccountFindFirst.mockResolvedValue({ id: 'existing' }) + const { insert } = transactionSpy() const result = await registerWithNewHousehold(INPUT) expect(result.success).toBe(false) - expect(tx.housingUnit.create).not.toHaveBeenCalled() + expect(insert).not.toHaveBeenCalled() }) it('still succeeds when the verification email cannot be sent', async () => { diff --git a/src/lib/auth/__tests__/login-by-code.test.ts b/src/lib/auth/__tests__/login-by-code.test.ts index 18299efe..d52090cf 100644 --- a/src/lib/auth/__tests__/login-by-code.test.ts +++ b/src/lib/auth/__tests__/login-by-code.test.ts @@ -13,17 +13,20 @@ import { BRANDS } from '@/lib/config/brand' import { RESIDENT_CODE_PREFIX } from '@/lib/auth/code-prefixes' -const mockUserFindUnique = jest.fn() +const mockUserFindFirst = jest.fn() const mockUserUpdate = jest.fn() -const mockResidentFindUnique = jest.fn() +const mockResidentFindFirst = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - user: { - findUnique: (...a: unknown[]) => mockUserFindUnique(...a), - update: (...a: unknown[]) => mockUserUpdate(...a), + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { findFirst: (...a: unknown[]) => mockUserFindFirst(...a) }, + resident: { findFirst: (...a: unknown[]) => mockResidentFindFirst(...a) }, }, - resident: { findUnique: (...a: unknown[]) => mockResidentFindUnique(...a) }, + update: () => ({ + set: (data: unknown) => ({ where: () => Promise.resolve(mockUserUpdate(data)) }), + }), }, })) @@ -39,10 +42,11 @@ jest.mock('@/lib/auth/rate-limit', () => ({ jest.mock('next/headers', () => ({ cookies: jest.fn() })) import { loginByCode } from '@/lib/auth' +import { eqParts } from '@/test-utils/drizzle-where' const STAFF = { id: 'u1', - email: 'staff@example.ch', + account: { email: 'staff@example.ch' }, name: 'Sam Staff', role: 'ADMIN' as const, active: true, @@ -50,9 +54,9 @@ const STAFF = { beforeEach(() => { jest.clearAllMocks() - mockUserFindUnique.mockResolvedValue(STAFF) + mockUserFindFirst.mockResolvedValue(STAFF) mockUserUpdate.mockResolvedValue({}) - mockResidentFindUnique.mockResolvedValue({ id: 'r1', code: 'RES-010' }) + mockResidentFindFirst.mockResolvedValue({ id: 'r1', code: 'RES-010' }) }) describe('loginByCode — staff routing', () => { @@ -63,9 +67,10 @@ describe('loginByCode — staff routing', () => { expect(result).toMatchObject({ success: true, type: 'staff' }) // Looked up by the exact string — a prefix never rewrites a code. - expect(mockUserFindUnique).toHaveBeenCalledWith( - expect.objectContaining({ where: { code: `${prefix}ADMIN1` } }), - ) + expect(eqParts(mockUserFindFirst.mock.calls[0][0].where)).toEqual({ + column: 'code', + value: `${prefix}ADMIN1`, + }) }, ) @@ -76,7 +81,7 @@ describe('loginByCode — staff routing', () => { }) it('rejects an inactive staff account', async () => { - mockUserFindUnique.mockResolvedValue({ ...STAFF, active: false }) + mockUserFindFirst.mockResolvedValue({ ...STAFF, active: false }) expect(await loginByCode('AOZH-ADMIN1', '10.0.0.1')).toMatchObject({ success: false }) }) }) @@ -86,7 +91,7 @@ describe('loginByCode — resident routing', () => { const result = await loginByCode(`${RESIDENT_CODE_PREFIX}010`, '10.0.0.1') expect(result).toMatchObject({ success: true, type: 'resident', code: 'RES-010' }) - expect(mockUserFindUnique).not.toHaveBeenCalled() + expect(mockUserFindFirst).not.toHaveBeenCalled() }) }) diff --git a/src/lib/auth/__tests__/staff-row-states-its-reach.test.ts b/src/lib/auth/__tests__/staff-row-states-its-reach.test.ts index a7544aff..83ada277 100644 --- a/src/lib/auth/__tests__/staff-row-states-its-reach.test.ts +++ b/src/lib/auth/__tests__/staff-row-states-its-reach.test.ts @@ -32,7 +32,7 @@ import fs from 'fs' import path from 'path' const REPO_ROOT = path.resolve(__dirname, '../../../..') -const SCAN_DIRS = ['src', 'prisma', 'scripts'] +const SCAN_DIRS = ['src', 'scripts'] /** The axes a staff row carries beyond its role. Add one here when one exists. */ const REACH_FIELDS = ['scope', 'isSystemAdmin'] as const @@ -89,32 +89,50 @@ function sourceFiles(): string[] { } /** - * The `data: { ... }` payload of each `prisma.user.create|upsert` in a file. + * The written payload of each `.insert(user).values(...)` chain in a file — + * the `.values(...)` argument plus, for upserts, the `.onConflictDoUpdate` + * argument. `.returning(...)` is deliberately EXCLUDED: it restates column + * names without writing them, so counting it would let a site pass by merely + * reading `scope` back. * - * Brace-counted rather than regex-matched: a payload contains nested objects - * (`account: { create: { ... } }`), and a non-greedy regex would stop at the - * first inner `}` and report a payload that states nothing. + * Paren-counted rather than regex-matched: a payload contains nested objects + * and calls, and a non-greedy regex would stop at the first inner `)` and + * report a payload that states nothing. */ function staffRowPayloads(source: string): string[] { const payloads: string[] = [] - const callSite = /prisma\.user\.(create|upsert)\s*\(/g + // Both spellings live in the tree: `insert(user)` in lib/scripts, and + // `insert(userTable)` where a route's local variable shadows the table name. + const callSite = /\.\s*insert\(\s*(?:user|userTable)\s*\)/g let match: RegExpExecArray | null while ((match = callSite.exec(source)) !== null) { - let depth = 0 - let end = source.length - for (let i = match.index + match[0].length - 1; i < source.length; i++) { - const ch = source[i] - if (ch === '(') depth++ - else if (ch === ')') { - depth-- - if (depth === 0) { - end = i - break + let payload = '' + let i = match.index + match[0].length + // Walk the fluent chain that follows the insert(). + for (;;) { + const link = /^\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/.exec(source.slice(i)) + if (!link) break + const open = i + link[0].length - 1 + let depth = 0 + let end = source.length + for (let j = open; j < source.length; j++) { + const ch = source[j] + if (ch === '(') depth++ + else if (ch === ')') { + depth-- + if (depth === 0) { + end = j + break + } } } + if (link[1] === 'values' || link[1] === 'onConflictDoUpdate') { + payload += source.slice(open + 1, end) + '\n' + } + i = end + 1 } - payloads.push(source.slice(match.index, end)) + if (payload) payloads.push(payload) } return payloads } @@ -122,7 +140,9 @@ function staffRowPayloads(source: string): string[] { function statesReach(payload: string): boolean { const spreads = payload.match(REACH_SPREAD) ?? [] if (spreads.some((spread) => REACH_NAME.test(spread))) return true - return REACH_FIELDS.every((field) => new RegExp(`\\b${field}\\s*:`).test(payload)) + // `[,:}\r\n]` and not just `:` — a payload may state a field in ES shorthand + // (`scope,`), the same syntax blind spot the role filter below documents. + return REACH_FIELDS.every((field) => new RegExp(`\\b${field}\\s*[,:}\\r\\n]`).test(payload)) } describe('every staff-row creation site states its reach', () => { diff --git a/src/lib/auth/__tests__/tokens.test.ts b/src/lib/auth/__tests__/tokens.test.ts index e8fda29d..2f386883 100644 --- a/src/lib/auth/__tests__/tokens.test.ts +++ b/src/lib/auth/__tests__/tokens.test.ts @@ -3,23 +3,39 @@ * invalidation of earlier tokens, and every consume failure mode. */ -const mockPrisma = { - authToken: { - deleteMany: jest.fn(), - create: jest.fn(), - findUnique: jest.fn(), - update: jest.fn(), +const mockAuthTokenFindFirst = jest.fn() +const mockInsertValues = jest.fn() +// (whereParts) — the delete that invalidates earlier tokens +const mockDeleteWhere = jest.fn() +// (data, whereParts) +const mockUpdate = jest.fn() + +jest.mock('@/lib/db', () => ({ + ...jest.requireActual('@/lib/db'), + db: { + query: { + authToken: { findFirst: (...a: unknown[]) => mockAuthTokenFindFirst(...a) }, + }, + insert: () => ({ values: (v: unknown) => Promise.resolve(mockInsertValues(v)) }), + delete: () => ({ + where: (w: unknown) => Promise.resolve(mockDeleteWhere(mockWhereParts(w))), + }), + update: () => ({ + set: (data: unknown) => ({ + where: (w: unknown) => Promise.resolve(mockUpdate(data, mockWhereParts(w))), + }), + }), }, -} -jest.mock('@/lib/db', () => ({ prisma: mockPrisma })) +})) import { createAuthToken, consumeAuthToken, hashAuthToken } from '../tokens' +import { whereParts as mockWhereParts } from '@/test-utils/drizzle-where' beforeEach(() => { jest.clearAllMocks() - mockPrisma.authToken.create.mockResolvedValue({}) - mockPrisma.authToken.deleteMany.mockResolvedValue({ count: 0 }) - mockPrisma.authToken.update.mockResolvedValue({}) + mockInsertValues.mockResolvedValue(undefined) + mockDeleteWhere.mockResolvedValue(undefined) + mockUpdate.mockResolvedValue(undefined) }) function validTokenRow(overrides: Record = {}) { @@ -36,7 +52,7 @@ function validTokenRow(overrides: Record = {}) { describe('createAuthToken', () => { it('stores only the SHA-256 hash, never the raw token', async () => { const raw = await createAuthToken('acc-1', 'RESET_PASSWORD') - const stored = mockPrisma.authToken.create.mock.calls[0][0].data + const stored = mockInsertValues.mock.calls[0][0] expect(stored.tokenHash).toBe(hashAuthToken(raw)) expect(stored.tokenHash).not.toBe(raw) expect(JSON.stringify(stored)).not.toContain(raw) @@ -44,15 +60,16 @@ describe('createAuthToken', () => { it('invalidates earlier tokens for the same account + purpose', async () => { await createAuthToken('acc-2', 'VERIFY_EMAIL') - expect(mockPrisma.authToken.deleteMany).toHaveBeenCalledWith({ - where: { accountId: 'acc-2', purpose: 'VERIFY_EMAIL' }, + expect(mockDeleteWhere).toHaveBeenCalledWith({ + accountId: 'acc-2', + purpose: 'VERIFY_EMAIL', }) }) it('gives reset tokens a 1-hour expiry', async () => { const before = Date.now() await createAuthToken('acc-1', 'RESET_PASSWORD') - const stored = mockPrisma.authToken.create.mock.calls[0][0].data + const stored = mockInsertValues.mock.calls[0][0] const ttl = stored.expiresAt.getTime() - before expect(ttl).toBeGreaterThan(59 * 60 * 1000) expect(ttl).toBeLessThanOrEqual(60 * 60 * 1000 + 1000) @@ -61,39 +78,36 @@ describe('createAuthToken', () => { describe('consumeAuthToken', () => { it('marks a valid token used and returns its account', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue(validTokenRow()) + mockAuthTokenFindFirst.mockResolvedValue(validTokenRow()) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBe('acc-1') - expect(mockPrisma.authToken.update).toHaveBeenCalledWith({ - where: { id: 'tok-1' }, - data: { usedAt: expect.any(Date) }, - }) + expect(mockUpdate).toHaveBeenCalledWith({ usedAt: expect.any(Date) }, { id: 'tok-1' }) }) it('rejects an unknown token', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue(null) + mockAuthTokenFindFirst.mockResolvedValue(null) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBeNull() }) it('rejects a token issued for a DIFFERENT purpose', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue(validTokenRow({ purpose: 'VERIFY_EMAIL' })) + mockAuthTokenFindFirst.mockResolvedValue(validTokenRow({ purpose: 'VERIFY_EMAIL' })) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBeNull() - expect(mockPrisma.authToken.update).not.toHaveBeenCalled() + expect(mockUpdate).not.toHaveBeenCalled() }) it('rejects an expired token', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue( + mockAuthTokenFindFirst.mockResolvedValue( validTokenRow({ expiresAt: new Date(Date.now() - 1000) }), ) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBeNull() }) it('rejects an already-used token (single use)', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue(validTokenRow({ usedAt: new Date() })) + mockAuthTokenFindFirst.mockResolvedValue(validTokenRow({ usedAt: new Date() })) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBeNull() }) it('returns whichever account the token belongs to', async () => { - mockPrisma.authToken.findUnique.mockResolvedValue(validTokenRow({ accountId: 'acc-9' })) + mockAuthTokenFindFirst.mockResolvedValue(validTokenRow({ accountId: 'acc-9' })) expect(await consumeAuthToken('raw', 'RESET_PASSWORD')).toBe('acc-9') }) }) diff --git a/src/lib/compatibility/__tests__/convert.test.ts b/src/lib/compatibility/__tests__/convert.test.ts index 7fe1c63f..8a796c8a 100644 --- a/src/lib/compatibility/__tests__/convert.test.ts +++ b/src/lib/compatibility/__tests__/convert.test.ts @@ -1,5 +1,5 @@ import { toResidentProfile } from '../convert' -import type { Resident } from '@prisma/client' +import type { Resident } from '@/lib/db' function makePrismaResident(overrides: Partial = {}): Resident { return { diff --git a/src/lib/compatibility/__tests__/placement-scores.test.ts b/src/lib/compatibility/__tests__/placement-scores.test.ts index 8bdca263..df7a5a09 100644 --- a/src/lib/compatibility/__tests__/placement-scores.test.ts +++ b/src/lib/compatibility/__tests__/placement-scores.test.ts @@ -7,7 +7,7 @@ */ import { calculateAverageScores } from '../placement-scores' -import type { Resident, Placement } from '@prisma/client' +import type { Resident, Placement } from '@/lib/db' // ============================================================================= // HELPERS diff --git a/src/lib/config/__tests__/appointment-statuses.test.ts b/src/lib/config/__tests__/appointment-statuses.test.ts index b4232c63..6a30f7ed 100644 --- a/src/lib/config/__tests__/appointment-statuses.test.ts +++ b/src/lib/config/__tests__/appointment-statuses.test.ts @@ -1,5 +1,4 @@ -import { readFileSync } from 'fs' -import { join } from 'path' +import * as dbSchema from '@/lib/db/schema' import { APPOINTMENT_STATUSES, APPOINTMENT_STATUS_BADGES, @@ -7,7 +6,7 @@ import { } from '@/lib/config/care' /** - * A config array and a Prisma enum are two spellings of one fact. + * A config array and a database enum are two spellings of one fact. * * `mapAppointment` casts the database value straight into `AppointmentStatusId`, * and a cast is not a check. A status that exists in the database and not here @@ -15,41 +14,34 @@ import { * tsc, ESLint and the page all green. That is exactly how REQUESTED could have * shipped: the migration adds it, the enum accepts it, and the UI shows nothing. * - * Reads the schema file rather than a hand-copied list, because a fixture would - * freeze the very drift it is meant to catch. + * Reads the drizzle schema's pgEnum rather than a hand-copied list, because a + * fixture would freeze the very drift it is meant to catch. */ -const SCHEMA_PATH = join(process.cwd(), 'prisma', 'schema.prisma') - -function enumValuesFromSchema(name: string): string[] { - const schema = readFileSync(SCHEMA_PATH, 'utf8') - const match = schema.match(new RegExp(`enum ${name} \\{([^}]*)\\}`)) - if (!match) throw new Error(`enum ${name} not found in prisma/schema.prisma`) - - return match[1] - .split('\n') - .map((line) => - line - .replace(/\/\/.*$/, '') - .replace(/\/\/\/.*$/, '') - .trim(), - ) - .filter((line) => line.length > 0) +function enumValuesFromDb(name: string): string[] { + for (const exported of Object.values(dbSchema)) { + if ( + typeof exported === 'function' && + 'enumName' in exported && + (exported as { enumName: string }).enumName === name + ) { + return [...(exported as unknown as { enumValues: string[] }).enumValues] + } + } + throw new Error(`enum ${name} not found in the drizzle schema`) } describe('appointment statuses match the database', () => { it('has exactly the values the schema declares', () => { // Sorted: declaration order is a display concern and differs legitimately. - expect([...APPOINTMENT_STATUSES].sort()).toEqual( - enumValuesFromSchema('AppointmentStatus').sort(), - ) + expect([...APPOINTMENT_STATUSES].sort()).toEqual(enumValuesFromDb('AppointmentStatus').sort()) }) it('actually reads the schema, and fails loudly when it cannot', () => { // Without this, a rename upstream turns the check above into a silent pass // over an empty list — the failed-fetch-as-fact trap. - expect(enumValuesFromSchema('AppointmentStatus').length).toBeGreaterThan(0) - expect(() => enumValuesFromSchema('NoSuchEnum')).toThrow(/not found/) + expect(enumValuesFromDb('AppointmentStatus').length).toBeGreaterThan(0) + expect(() => enumValuesFromDb('NoSuchEnum')).toThrow(/not found/) }) it('labels and badges every status, so none can render blank', () => { diff --git a/src/lib/config/__tests__/decision-mode-voting.test.ts b/src/lib/config/__tests__/decision-mode-voting.test.ts index b00ebdc8..b3ddc8f9 100644 --- a/src/lib/config/__tests__/decision-mode-voting.test.ts +++ b/src/lib/config/__tests__/decision-mode-voting.test.ts @@ -7,7 +7,7 @@ */ import { CATEGORY_DECISION_MODE, DECISION_MODE_IS_VOTED } from '../decisions' -import type { DecisionMode } from '@prisma/client' +import type { DecisionMode } from '@/lib/db' describe('DECISION_MODE_IS_VOTED', () => { it('marks staff-decided proposals as never voted on', () => { diff --git a/src/lib/config/__tests__/infra-ssot.test.ts b/src/lib/config/__tests__/infra-ssot.test.ts index c8a81331..0d18705a 100644 --- a/src/lib/config/__tests__/infra-ssot.test.ts +++ b/src/lib/config/__tests__/infra-ssot.test.ts @@ -3,8 +3,8 @@ import { resolve } from 'path' /** * Guards the failure mode that sent an agent at a dead cloud host: tracked - * env templates and the Prisma client describing production as a pooler, or - * naming the wrong database. Docs may name the decommissioned host as a + * env templates and the db client module describing production as a pooler, + * or naming the wrong database. Docs may name the decommissioned host as a * negative example so a search for it finds "that is stale". */ @@ -15,8 +15,8 @@ function read(relative: string) { } describe('production is Hetzner Postgres, not a cloud pooler', () => { - it('does not put a cloud-pooler host in the env template or Prisma client', () => { - for (const file of ['.env.example', 'src/lib/db.ts'] as const) { + it('does not put a cloud-pooler host in the env template or db client', () => { + for (const file of ['.env.example', 'src/lib/db/index.ts'] as const) { const text = read(file) expect({ file, hasNeonHost: /neon\.tech|neondb|@neondatabase/.test(text) }).toEqual({ file, diff --git a/src/lib/config/care.ts b/src/lib/config/care.ts index 1cb5ecc0..1885b24b 100644 --- a/src/lib/config/care.ts +++ b/src/lib/config/care.ts @@ -54,7 +54,7 @@ export const CARE_ROLE_LABELS: Record = { } /** - * Must match the `AppointmentStatus` enum in schema.prisma exactly. + * Must match the `AppointmentStatus` pgEnum in src/lib/db/schema.ts exactly. * * `mapAppointment` casts the database value into this union, and a cast is not * a check: a status present in the database and missing here renders its label diff --git a/src/lib/config/conflict-resolution.ts b/src/lib/config/conflict-resolution.ts index c6d33b5c..00f74e79 100644 --- a/src/lib/config/conflict-resolution.ts +++ b/src/lib/config/conflict-resolution.ts @@ -16,12 +16,7 @@ * did not hold — it says nothing about who was at fault. */ -import type { - IncidentCategory, - IncidentSeverity, - ResolutionStage, - AgreementStatus, -} from '@/lib/db' +import type { IncidentCategory, IncidentSeverity, ResolutionStage, AgreementStatus } from '@/lib/db' // ============================================================================= // THE LADDER diff --git a/src/lib/config/housing-factors.ts b/src/lib/config/housing-factors.ts index b318c483..1b39f4cf 100644 --- a/src/lib/config/housing-factors.ts +++ b/src/lib/config/housing-factors.ts @@ -5,8 +5,8 @@ * * To add a new factor: * 1. Add to HOUSING_FACTORS below - * 2. Add corresponding field to Prisma schema (if not using JSON storage) - * 3. Run prisma migrate + * 2. Add corresponding column in src/lib/db/schema.ts (if not using JSON storage) + * 3. Run npm run db:generate + db:migrate * * Everything else (forms, labels, matching) auto-generates from this config. */ diff --git a/src/lib/config/marketplace.ts b/src/lib/config/marketplace.ts index 95a1f970..5a7defb9 100644 --- a/src/lib/config/marketplace.ts +++ b/src/lib/config/marketplace.ts @@ -59,7 +59,7 @@ interface MarketplaceKindConfig { } /** - * Every kind, keyed by the Prisma enum so a value added to the schema without + * Every kind, keyed by the database enum so a value added to the schema without * a config entry fails to compile rather than rendering a blank chip. */ export const MARKETPLACE_KINDS: Record = { diff --git a/src/lib/config/thresholds.ts b/src/lib/config/thresholds.ts index e580b799..385437cc 100644 --- a/src/lib/config/thresholds.ts +++ b/src/lib/config/thresholds.ts @@ -230,7 +230,7 @@ export function getHealthLevel(score: number): HealthLevel { // ============================================================================= /** - * Row limits for Prisma queries, grouped by semantic context. + * Row limits for database queries, grouped by semantic context. * Distinct from DISPLAY_LIMITS (which cap rendered lists after data is fetched). */ export const QUERY_LIMITS = { diff --git a/src/lib/data/__tests__/opportunities-board.test.ts b/src/lib/data/__tests__/opportunities-board.test.ts index 1ace4e13..669caf0c 100644 --- a/src/lib/data/__tests__/opportunities-board.test.ts +++ b/src/lib/data/__tests__/opportunities-board.test.ts @@ -8,17 +8,26 @@ * of the returned object is asserted directly. */ -import { prisma } from '@/lib/db' -import { residentOpportunityBoard } from '../opportunities' +const mockApplicationFindMany = jest.fn() +const mockOpportunityFindMany = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - opportunityApplication: { findMany: jest.fn() }, - opportunity: { findMany: jest.fn() }, + ...jest.requireActual('@/lib/db'), + db: { + query: { + opportunityApplication: { findMany: (...a: unknown[]) => mockApplicationFindMany(...a) }, + opportunity: { findMany: (...a: unknown[]) => mockOpportunityFindMany(...a) }, + }, }, })) -const mockPrisma = prisma as jest.Mocked +import { residentOpportunityBoard } from '../opportunities' +import { whereParts } from '@/test-utils/drizzle-where' + +const mockDb = { + opportunityApplication: { findMany: mockApplicationFindMany }, + opportunity: { findMany: mockOpportunityFindMany }, +} function listing(overrides: Record = {}) { return { @@ -35,8 +44,8 @@ function listing(overrides: Record = {}) { beforeEach(() => { jest.clearAllMocks() - ;(mockPrisma.opportunityApplication.findMany as jest.Mock).mockResolvedValue([]) - ;(mockPrisma.opportunity.findMany as jest.Mock).mockResolvedValue([listing()]) + ;(mockDb.opportunityApplication.findMany as jest.Mock).mockResolvedValue([]) + ;(mockDb.opportunity.findMany as jest.Mock).mockResolvedValue([listing()]) }) describe('residentOpportunityBoard', () => { @@ -56,7 +65,7 @@ describe('residentOpportunityBoard', () => { it('reports an unstated capacity as unknown, not as full', async () => { // `null` seats means the listing never named a number. Rendering that as // zero would take an open place off the board for everyone. - ;(mockPrisma.opportunity.findMany as jest.Mock).mockResolvedValue([listing({ seats: null })]) + ;(mockDb.opportunity.findMany as jest.Mock).mockResolvedValue([listing({ seats: null })]) const { open } = await residentOpportunityBoard('res-1') @@ -66,14 +75,14 @@ describe('residentOpportunityBoard', () => { it('only ever asks for published listings', async () => { await residentOpportunityBoard('res-1') - const where = (mockPrisma.opportunity.findMany as jest.Mock).mock.calls[0][0].where - expect(where).toEqual({ status: 'PUBLISHED' }) + const where = (mockDb.opportunity.findMany as jest.Mock).mock.calls[0][0].where + expect(whereParts(where)).toEqual({ status: 'PUBLISHED' }) }) it('drops listings the resident is already attached to', async () => { // Otherwise the same place appears twice — once under "your placements" // with a stage, and once under "open" with a button that cannot work. - ;(mockPrisma.opportunityApplication.findMany as jest.Mock).mockResolvedValue([ + ;(mockDb.opportunityApplication.findMany as jest.Mock).mockResolvedValue([ { id: 'app-1', opportunityId: 'opp-1', stage: 'INTERESTED', opportunity: listing() }, ]) @@ -84,7 +93,7 @@ describe('residentOpportunityBoard', () => { }) it('puts the places someone can still take first', async () => { - ;(mockPrisma.opportunity.findMany as jest.Mock).mockResolvedValue([ + ;(mockDb.opportunity.findMany as jest.Mock).mockResolvedValue([ listing({ id: 'full', seats: 1, applications: [{ stage: 'STARTED' }] }), listing({ id: 'open', seats: 2, applications: [] }), ]) @@ -97,7 +106,7 @@ describe('residentOpportunityBoard', () => { it('scopes the applications it returns to the asking resident', async () => { await residentOpportunityBoard('res-1') - const where = (mockPrisma.opportunityApplication.findMany as jest.Mock).mock.calls[0][0].where - expect(where).toEqual({ residentId: 'res-1' }) + const where = (mockDb.opportunityApplication.findMany as jest.Mock).mock.calls[0][0].where + expect(whereParts(where)).toEqual({ residentId: 'res-1' }) }) }) diff --git a/src/lib/demo/__tests__/reset.test.ts b/src/lib/demo/__tests__/reset.test.ts index eb2c89a1..1b7c811b 100644 --- a/src/lib/demo/__tests__/reset.test.ts +++ b/src/lib/demo/__tests__/reset.test.ts @@ -24,7 +24,9 @@ jest.mock('../../seed/opportunities', () => ({ import { resetDemoData } from '../reset' import { STAFF_ROLES } from '@/lib/auth/role-policy' -import type { PrismaClient } from '@prisma/client' +import { account, user, type db } from '@/lib/db' +import { eq } from 'drizzle-orm' +import { sqlText } from '@/test-utils/drizzle-where' const SEED_SUMMARY = { residents: 15, @@ -34,23 +36,57 @@ const SEED_SUMMARY = { demoResidentCode: 'RES-001', } -function createPrismaMock(tables: string[]) { - return { - $queryRaw: jest.fn().mockResolvedValue(tables.map((tablename) => ({ tablename }))), - $executeRawUnsafe: jest.fn().mockResolvedValue(0), - user: { upsert: jest.fn().mockResolvedValue({ id: 'demo-user' }) }, - account: { deleteMany: jest.fn().mockResolvedValue({ count: 0 }) }, +/** + * A drizzle-client stand-in built to the surface the reset actually uses: + * `execute` (pg_tables SELECT, then the TRUNCATE), the demo-staff upsert + * (`insert(user)…onConflictDoUpdate…returning`), `delete(account)`, and the + * resident listing the opportunity seed reads. + */ +function createDbMock(tables: string[]) { + // Records the TRUNCATE statement's text; the pg_tables SELECT answers with + // the given table list instead. + const executeTruncate = jest.fn((statement: string) => { + void statement + return Promise.resolve({ rows: [] as unknown[] }) + }) + // Records each staff upsert as { values, target, set }. + const userUpsert = jest.fn((call: unknown) => { + void call + return [{ id: 'demo-user' }] + }) + const accountDelete = jest.fn().mockResolvedValue({ rowCount: 0 }) + const residentFindMany = jest.fn().mockResolvedValue([{ id: 'demo-resident-1' }]) + + const dbMock = { + execute: jest.fn((query: unknown) => { + const text = sqlText(query) + if (text.includes('pg_tables')) { + return Promise.resolve({ rows: tables.map((tablename) => ({ tablename })) }) + } + return executeTruncate(text) + }), + insert: () => ({ + values: (values: unknown) => ({ + onConflictDoUpdate: ({ target, set }: { target: unknown; set: unknown }) => ({ + returning: () => Promise.resolve(userUpsert({ values, target, set })), + }), + }), + }), + delete: () => ({ where: (w: unknown) => accountDelete(w) }), // The org-wide opportunity directory is seeded from the demo residents // this path just created. Unscoped is correct HERE and only here: the // wipe above ran first, so every remaining resident is a demo resident. - resident: { - findMany: jest.fn().mockResolvedValue([{ id: 'demo-resident-1' }]), + query: { + resident: { findMany: (...args: unknown[]) => residentFindMany(...args) }, }, - } as unknown as PrismaClient & { - $executeRawUnsafe: jest.Mock - user: { upsert: jest.Mock } - account: { deleteMany: jest.Mock } - resident: { findMany: jest.Mock } + } + + return { + db: dbMock as unknown as typeof db, + executeTruncate, + userUpsert, + accountDelete, + residentFindMany, } } @@ -72,11 +108,11 @@ describe('resetDemoData', () => { it('seeds the opportunity directory from the residents it just created', async () => { // Mocking a seed away and then never asserting it ran is how a step // silently stops happening while the suite stays green. - const prisma = createPrismaMock(['Resident']) - const summary = await resetDemoData(prisma) + const { db } = createDbMock(['Resident']) + const summary = await resetDemoData(db) expect(mockSeedOpportunities).toHaveBeenCalledWith( - prisma, + db, expect.objectContaining({ residentIds: ['demo-resident-1'], staffId: 'demo-user' }), ) expect(summary.opportunities).toBe(5) @@ -88,7 +124,7 @@ describe('resetDemoData', () => { }) it('truncates every discovered table except the keep-list', async () => { - const prisma = createPrismaMock([ + const { db, executeTruncate } = createDbMock([ '_prisma_migrations', 'User', 'AlgorithmWeight', @@ -99,10 +135,10 @@ describe('resetDemoData', () => { 'BrandNewFutureModel', // schema growth: wiped without editing the reset ]) - await resetDemoData(prisma) + await resetDemoData(db) - expect(prisma.$executeRawUnsafe).toHaveBeenCalledTimes(1) - const sql = prisma.$executeRawUnsafe.mock.calls[0][0] as string + expect(executeTruncate).toHaveBeenCalledTimes(1) + const sql = executeTruncate.mock.calls[0][0] as string expect(sql).toContain('"Resident"') expect(sql).toContain('"HousingUnit"') expect(sql).toContain('"HouseRule"') @@ -115,34 +151,33 @@ describe('resetDemoData', () => { }) it('reseeds, self-heals the demo staff account, and re-syncs the rule catalog', async () => { - const prisma = createPrismaMock(['Resident']) + const { db, userUpsert, accountDelete } = createDbMock(['Resident']) - const summary = await resetDemoData(prisma) + const summary = await resetDemoData(db) // The staff account is upserted BEFORE the seed and handed to it: the // care seats it assigns cannot point at a row that does not exist yet. - expect(mockSeedDemoData).toHaveBeenCalledWith(prisma, { + expect(mockSeedDemoData).toHaveBeenCalledWith(db, { careStaffId: 'demo-user', // Full scope owns the whole database, so it may also create content no // demo prefix reaches — and truncate it next time round. The scoped // reset must NOT pass this; `scoped-reset.test.ts` holds that end. siteWideContent: true, }) - expect(prisma.user.upsert).toHaveBeenCalledWith({ - where: { code: 'AOZH-DEMO01' }, - update: { name: 'Demo-Zugang', active: true, scope: 'ALL_DOMAINS', isSystemAdmin: true }, - create: { + expect(userUpsert).toHaveBeenCalledWith({ + values: { code: 'AOZH-DEMO01', name: 'Demo-Zugang', role: 'ADMIN', scope: 'ALL_DOMAINS', isSystemAdmin: true, }, - select: { id: true }, + target: user.code, + set: { name: 'Demo-Zugang', active: true, scope: 'ALL_DOMAINS', isSystemAdmin: true }, }) // A visitor-claimed account on the demo code must not outlive the reset. - expect(prisma.account.deleteMany).toHaveBeenCalledWith({ where: { userId: 'demo-user' } }) - expect(mockSyncOrgRules).toHaveBeenCalledWith(prisma) + expect(accountDelete).toHaveBeenCalledWith(eq(account.userId, 'demo-user')) + expect(mockSyncOrgRules).toHaveBeenCalledWith(db) expect(summary).toEqual({ ...SEED_SUMMARY, tablesWiped: 1, @@ -160,11 +195,11 @@ describe('resetDemoData', () => { // derived code — it does not mean "no staff demo", which is what this used // to assert back when the single door was the whole feature. delete process.env.DEMO_STAFF_CODE - const prisma = createPrismaMock(['Resident']) + const { db, userUpsert } = createDbMock(['Resident']) - const summary = await resetDemoData(prisma) + const summary = await resetDemoData(db) - expect(prisma.user.upsert).toHaveBeenCalledTimes(STAFF_ROLES.length) + expect(userUpsert).toHaveBeenCalledTimes(STAFF_ROLES.length) // The legacy single-door summary field stays null: nothing was configured. expect(summary.demoStaffCode).toBeNull() }) diff --git a/src/lib/demo/__tests__/scoped-reset.test.ts b/src/lib/demo/__tests__/scoped-reset.test.ts index 5c74a3fb..45dcdf6f 100644 --- a/src/lib/demo/__tests__/scoped-reset.test.ts +++ b/src/lib/demo/__tests__/scoped-reset.test.ts @@ -19,6 +19,7 @@ jest.mock('../../governance/sync-org-rules', () => ({ syncOrgRules: (...args: unknown[]) => mockSyncOrgRules(...args), })) +import { getTableName, eq, inArray, like, or, type SQL } from 'drizzle-orm' import { deleteDemoWorld, resetDemoWorld } from '../scoped-reset' import { upsertDemoStaff } from '../staff' import { @@ -26,27 +27,75 @@ import { DEMO_RESIDENT_CODE_PREFIX, DEMO_UNIT_CODE_PREFIX, } from '../config' -import type { PrismaClient } from '@prisma/client' +import { + escapeLike, + account, + housingUnit, + incident, + message, + messageThread, + placement, + resident, + user, + type db, +} from '@/lib/db' -function createPrismaMock() { +/** + * A drizzle-client stand-in for the scoped reset's surface: prefix-filtered + * `select` subqueries, ordered `delete(table).where(...)` calls, and the + * demo-staff upsert. `select(...).from(...).where(f)` returns a marker object + * carrying the filter, so a delete's `inArray(col, subquery)` tree can be + * compared against one built with the same marker. + */ +function createDbMock() { const calls: string[] = [] - const track = (name: string, result: unknown) => - jest.fn(() => { - calls.push(name) - return Promise.resolve(result) - }) + const deletes: Record = {} + const rowCounts: Record = { + Incident: 7, + Placement: 13, + HousingUnit: 5, + Message: 4, + MessageThread: 2, + Resident: 15, + Account: 0, + } + const userUpsert = jest.fn((call: unknown) => { + void call + calls.push('user.upsert') + return [{ id: 'demo-user' }] + }) - const prisma = { - incident: { deleteMany: track('incident.deleteMany', { count: 7 }) }, - placement: { deleteMany: track('placement.deleteMany', { count: 13 }) }, - housingUnit: { deleteMany: track('unit.deleteMany', { count: 5 }) }, - message: { deleteMany: track('message.deleteMany', { count: 4 }) }, - messageThread: { deleteMany: track('messageThread.deleteMany', { count: 2 }) }, - resident: { deleteMany: track('resident.deleteMany', { count: 15 }) }, - user: { upsert: track('user.upsert', { id: 'demo-user' }) }, - account: { deleteMany: track('account.deleteMany', { count: 0 }) }, + const dbMock = { + select: () => ({ + from: () => ({ where: (filter: unknown) => ({ __subquery: filter }) }), + }), + delete: (table: unknown) => ({ + where: (w: unknown) => { + const name = getTableName(table as Parameters[0]) + calls.push(`${name === 'Account' ? 'account' : name}.delete`) + ;(deletes[name] ??= []).push(w) + return Promise.resolve({ rowCount: rowCounts[name] ?? 0 }) + }, + }), + insert: () => ({ + values: (values: unknown) => ({ + onConflictDoUpdate: ({ target, set }: { target: unknown; set: unknown }) => ({ + returning: () => Promise.resolve(userUpsert({ values, target, set })), + }), + }), + }), } - return { prisma: prisma as unknown as PrismaClient, calls, raw: prisma } + + return { db: dbMock as unknown as typeof db, calls, deletes, userUpsert } +} + +/** + * The subquery marker the mock's `select` hands back for a given filter. + * Cast to SQL so it can sit where `inArray` expects a subquery — the + * comparison is structural (toEqual), so only the shape matters. + */ +function subqueryOf(filter: unknown): SQL { + return { __subquery: filter } as unknown as SQL } const SEED_SUMMARY = { @@ -57,6 +106,15 @@ const SEED_SUMMARY = { demoResidentCode: `${DEMO_RESIDENT_CODE_PREFIX}1`, } +const unitFilter = () => like(housingUnit.code, `${escapeLike(DEMO_UNIT_CODE_PREFIX)}%`) +const residentFilter = (configuredCode: string) => + or( + ...ALL_DEMO_RESIDENT_CODE_PREFIXES.map((prefix) => + like(resident.code, `${escapeLike(prefix)}%`), + ), + eq(resident.code, configuredCode), + ) + beforeEach(() => { jest.clearAllMocks() delete process.env.DEMO_RESIDENT_CODE @@ -71,91 +129,71 @@ describe('deleteDemoWorld', () => { // key, so getting this wrong does not surface as "you forgot messages" — // it surfaces as the whole nightly reset failing and the demo rotting from // that day on. - const { prisma, calls } = createPrismaMock() - await deleteDemoWorld(prisma) + const { db, calls } = createDbMock() + await deleteDemoWorld(db) expect(calls).toEqual([ - 'incident.deleteMany', - 'placement.deleteMany', - 'unit.deleteMany', - 'message.deleteMany', - 'messageThread.deleteMany', - 'resident.deleteMany', + 'Incident.delete', + 'Placement.delete', + 'HousingUnit.delete', + 'Message.delete', + 'MessageThread.delete', + 'Resident.delete', ]) }) it('scopes message deletion to demo residents, never the whole table', async () => { // The demo world lives ALONGSIDE a real flat on this deployment. A delete // without this filter would erase real conversations. - const { prisma, raw } = createPrismaMock() - await deleteDemoWorld(prisma) + const { db, deletes } = createDbMock() + await deleteDemoWorld(db) // EVERY demo prefix the product has ever issued, not just today's — a demo // resident seeded under an earlier client prefix must still be reachable // by the reset, or it survives forever beside the real flat. - const demoResidentFilter = { - OR: [ - ...ALL_DEMO_RESIDENT_CODE_PREFIXES.map((prefix) => ({ code: { startsWith: prefix } })), - { code: `${DEMO_RESIDENT_CODE_PREFIX}1` }, - ], - } - - expect((raw.message.deleteMany as jest.Mock).mock.calls[0][0]).toEqual({ - where: { authorResident: demoResidentFilter }, - }) - expect((raw.messageThread.deleteMany as jest.Mock).mock.calls[0][0]).toEqual({ - where: { resident: demoResidentFilter }, - }) + const demoResidents = subqueryOf(residentFilter(`${DEMO_RESIDENT_CODE_PREFIX}1`)) + + expect(deletes.Message[0]).toEqual(inArray(message.authorResidentId, demoResidents)) + expect(deletes.MessageThread[0]).toEqual(inArray(messageThread.residentId, demoResidents)) }) it('targets units only by the demo code prefix — never a whole table', async () => { - const { prisma, raw } = createPrismaMock() - await deleteDemoWorld(prisma) - const unitFilter = { code: { startsWith: DEMO_UNIT_CODE_PREFIX } } - expect((raw.housingUnit.deleteMany as jest.Mock).mock.calls[0][0]).toEqual({ - where: unitFilter, - }) - expect((raw.incident.deleteMany as jest.Mock).mock.calls[0][0]).toEqual({ - where: { housingUnit: unitFilter }, - }) - expect((raw.placement.deleteMany as jest.Mock).mock.calls[0][0]).toEqual({ - where: { housingUnit: unitFilter }, - }) + const { db, deletes } = createDbMock() + await deleteDemoWorld(db) + const demoUnits = subqueryOf(unitFilter()) + expect(deletes.HousingUnit[0]).toEqual(unitFilter()) + expect(deletes.Incident[0]).toEqual(inArray(incident.housingUnitId, demoUnits)) + expect(deletes.Placement[0]).toEqual(inArray(placement.housingUnitId, demoUnits)) }) it('only ever targets demo residents (prefix or configured login code)', async () => { process.env.DEMO_RESIDENT_CODE = 'RES-SPECIAL' - const { prisma, raw } = createPrismaMock() - await deleteDemoWorld(prisma) - expect((raw.resident.deleteMany as jest.Mock).mock.calls[0][0].where).toEqual({ - OR: [ - ...ALL_DEMO_RESIDENT_CODE_PREFIXES.map((prefix) => ({ code: { startsWith: prefix } })), - { code: 'RES-SPECIAL' }, - ], - }) + const { db, deletes } = createDbMock() + await deleteDemoWorld(db) + expect(deletes.Resident[0]).toEqual(residentFilter('RES-SPECIAL')) }) it('reports what it removed', async () => { - const { prisma } = createPrismaMock() - expect(await deleteDemoWorld(prisma)).toEqual({ unitsDeleted: 5, residentsDeleted: 15 }) + const { db } = createDbMock() + expect(await deleteDemoWorld(db)).toEqual({ unitsDeleted: 5, residentsDeleted: 15 }) }) }) describe('resetDemoWorld', () => { it('tears down before seeding, then self-heals the staff account', async () => { process.env.DEMO_STAFF_CODE = 'WG-DEMO01' - const { prisma, calls } = createPrismaMock() - const summary = await resetDemoWorld(prisma) + const { db, calls } = createDbMock() + const summary = await resetDemoWorld(db) expect(calls).toEqual([ - 'incident.deleteMany', - 'placement.deleteMany', - 'unit.deleteMany', - 'message.deleteMany', - 'messageThread.deleteMany', - 'resident.deleteMany', + 'Incident.delete', + 'Placement.delete', + 'HousingUnit.delete', + 'Message.delete', + 'MessageThread.delete', + 'Resident.delete', 'user.upsert', - 'account.deleteMany', + 'account.delete', ]) expect(mockSeedDemoData).toHaveBeenCalledTimes(1) // The other end of the safety property the full reset test pins. This @@ -175,40 +213,37 @@ describe('resetDemoWorld', () => { }) it('touches no staff account when no staff door is configured', async () => { - const { prisma, raw } = createPrismaMock() - const summary = await resetDemoWorld(prisma) + const { db, userUpsert } = createDbMock() + const summary = await resetDemoWorld(db) expect(summary.demoStaffCode).toBeNull() - expect(raw.user.upsert as jest.Mock).not.toHaveBeenCalled() + expect(userUpsert).not.toHaveBeenCalled() }) }) describe('upsertDemoStaff', () => { it('upserts the dedicated demo admin under the configured code', async () => { process.env.DEMO_STAFF_CODE = 'WG-DEMO01' - const { prisma, raw } = createPrismaMock() - expect(await upsertDemoStaff(prisma)).toEqual({ id: 'demo-user', code: 'WG-DEMO01' }) - expect((raw.user.upsert as jest.Mock).mock.calls[0][0]).toEqual({ - where: { code: 'WG-DEMO01' }, - update: { name: 'Demo-Zugang', active: true, scope: 'ALL_DOMAINS', isSystemAdmin: true }, - create: { + const { db, userUpsert } = createDbMock() + expect(await upsertDemoStaff(db)).toEqual({ id: 'demo-user', code: 'WG-DEMO01' }) + expect(userUpsert.mock.calls[0][0]).toEqual({ + values: { code: 'WG-DEMO01', name: 'Demo-Zugang', role: 'ADMIN', scope: 'ALL_DOMAINS', isSystemAdmin: true, }, - select: { id: true }, + target: user.code, + set: { name: 'Demo-Zugang', active: true, scope: 'ALL_DOMAINS', isSystemAdmin: true }, }) }) it('drops any account a visitor claimed on the demo code', async () => { process.env.DEMO_STAFF_CODE = 'WG-DEMO01' - const { prisma, raw } = createPrismaMock() - await upsertDemoStaff(prisma) + const { db, deletes } = createDbMock() + await upsertDemoStaff(db) // A visitor-claimed email/password on the demo door must not outlive the // reset — otherwise the next tester cannot get in. - expect(raw.account.deleteMany as jest.Mock).toHaveBeenCalledWith({ - where: { userId: 'demo-user' }, - }) + expect(deletes.Account[0]).toEqual(eq(account.userId, 'demo-user')) }) }) diff --git a/src/lib/demo/__tests__/seed-data.test.ts b/src/lib/demo/__tests__/seed-data.test.ts index 2d90d425..e4104189 100644 --- a/src/lib/demo/__tests__/seed-data.test.ts +++ b/src/lib/demo/__tests__/seed-data.test.ts @@ -7,19 +7,22 @@ * row that survives every reset and can never be cleaned up. */ +import { getTableName } from 'drizzle-orm' import { seedDemoData } from '../seed-data' import { DEMO_RESIDENT_CODE_PREFIX, DEMO_UNIT_CODE_PREFIX, resolveDemoResidentCode, } from '../config' -import type { PrismaClient } from '@prisma/client' +import type { db } from '@/lib/db' import { natureOfKind } from '../../config/marketplace' interface Recorded { unitCodes: string[] residentCodes: string[] - expenses: Array<{ amountRappen: number; shares: { create: Array<{ amountRappen: number }> } }> + /** Expense rows and their shares, joined by id in the assertion below. */ + expenses: Array<{ id: string; amountRappen: number }> + expenseShares: Array<{ expenseId: string; amountRappen: number }> taskTitles: string[] proposalStatuses: string[] appointmentStatuses: string[] @@ -40,11 +43,12 @@ interface Recorded { activityCategories: string[] } -function createPrismaMock(): { prisma: PrismaClient; recorded: Recorded } { +function createDbMock(): { db: typeof db; recorded: Recorded } { const recorded: Recorded = { unitCodes: [], residentCodes: [], expenses: [], + expenseShares: [], taskTitles: [], proposalStatuses: [], appointmentStatuses: [], @@ -60,77 +64,86 @@ function createPrismaMock(): { prisma: PrismaClient; recorded: Recorded } { } let id = 0 - // A small STORE, not a set of empty stubs: `findMany` returns what was - // created. Filtering is deliberately unimplemented — the seed's only - // findMany asks for every demo-prefixed resident, and the test below proves - // every resident the seed creates carries that prefix, so "everything" and - // "everything matching" are the same set here. - const model = (onCreate?: (data: Record, newId: string) => void) => { - const rows: Array> = [] - return { - create: jest.fn(({ data }: { data: Record }) => { - const newId = `id-${++id}` - rows.push({ ...data, id: newId }) - onCreate?.(data, newId) - return Promise.resolve({ ...data, id: newId }) - }), - createMany: jest.fn(({ data }: { data: Array> }) => { - // Same recording hook as `create`. Without this a model written via - // createMany records nothing, and an assertion about it passes on an - // empty array — green because it never looked. - for (const row of data) { - const newId = `id-${++id}` - rows.push({ ...row, id: newId }) - onCreate?.(row, newId) - } - return Promise.resolve({ count: data.length }) - }), - count: jest.fn(() => Promise.resolve(0)), - findMany: jest.fn(() => Promise.resolve(rows)), - } - } + type Row = Record - const prisma = { - housingUnit: model((d) => recorded.unitCodes.push(d.code as string)), - resident: model((d, newId) => { + // Recording hooks per table, fed the stored row (values + generated id). + const hooks: Record void> = { + HousingUnit: (d) => recorded.unitCodes.push(d.code as string), + Resident: (d) => { recorded.residentCodes.push(d.code as string) - recorded.residentIdsByCode[d.code as string] = newId + recorded.residentIdsByCode[d.code as string] = d.id as string recorded.residentNamesByCode[d.code as string] = (d.displayName as string | undefined) ?? null - }), - placementSpot: model(), - placement: model(), - incident: model(), - incidentInvolvement: model(), - expense: model((d) => recorded.expenses.push(d as Recorded['expenses'][number])), - settlement: model(), + }, + Expense: (d) => + recorded.expenses.push({ id: d.id as string, amountRappen: d.amountRappen as number }), + ExpenseShare: (d) => + recorded.expenseShares.push({ + expenseId: d.expenseId as string, + amountRappen: d.amountRappen as number, + }), // The living-together half of the demo world (seed-governance.ts). - householdTask: model((d) => recorded.taskTitles.push(d.title as string)), - taskCompletion: model(), - taskAttentionFlag: model(), - proposal: model((d) => recorded.proposalStatuses.push(d.status as string)), - maintenanceRequest: model((d) => { + HouseholdTask: (d) => recorded.taskTitles.push(d.title as string), + Proposal: (d) => recorded.proposalStatuses.push(d.status as string), + MaintenanceRequest: (d) => { recorded.maintenanceStatuses.push(d.status as string) recorded.maintenance.push(d as Recorded['maintenance'][number]) - }), + }, // The integration pillar (lib/seed/integration-evidence.ts). - learningRecord: model((d) => recorded.learningResidentIds.push(d.residentId as string)), - careAssignment: model(), + LearningRecord: (d) => recorded.learningResidentIds.push(d.residentId as string), // Appointments, and the reading a completed one carries. Absent from - // this mock the seed threw on `appointment.create` — which is the mock + // this mock the seed threw on `insert(appointment)` — which is the mock // telling the truth: the seed genuinely writes these now. - appointment: model((d) => recorded.appointmentStatuses.push(d.status as string)), - satisfactionCheckIn: model(), + Appointment: (d) => recorded.appointmentStatuses.push(d.status as string), // Gemeinschaft: the board, the calendar and the external catalogue. - marketplacePost: model((d) => recorded.marketplaceKinds.push(d.kind as string)), - houseEvent: model((d) => recorded.eventStartsAt.push(d.startsAt as Date)), - eventRsvp: model(), - activity: model((d) => recorded.activityCategories.push(d.category as string)), - houseRule: { - ...model((d) => recorded.unitRuleTitles.push(d.title as string)), - findUnique: jest.fn(() => Promise.resolve({ id: 'org-night-quiet' })), + MarketplacePost: (d) => recorded.marketplaceKinds.push(d.kind as string), + HouseEvent: (d) => recorded.eventStartsAt.push(d.startsAt as Date), + Activity: (d) => recorded.activityCategories.push(d.category as string), + HouseRule: (d) => recorded.unitRuleTitles.push(d.title as string), + } + + // A small STORE, not a set of empty stubs: `findMany` returns what was + // inserted. Filtering is deliberately unimplemented — the seed's findMany + // calls ask for every demo-prefixed resident (or their placements), and the + // test below proves every resident the seed creates carries that prefix, so + // "everything" and "everything matching" are the same set here. + const store: Record = {} + + const insert = (table: unknown) => { + const name = getTableName(table as Parameters[0]) + return { + values: (data: unknown) => { + const rows = (Array.isArray(data) ? data : [data]).map((values: Row) => { + const row = { id: `id-${++id}`, ...values } + ;(store[name] ??= []).push(row) + hooks[name]?.(row) + return row + }) + // `.values()` alone awaits to a pg result; `.returning()` yields the + // rows; `.onConflictDoNothing()` still reports the write's rowCount. + return Object.assign(Promise.resolve({ rowCount: rows.length }), { + returning: () => Promise.resolve(rows), + onConflictDoNothing: () => Promise.resolve({ rowCount: rows.length }), + }) + }, + } + } + + const dbMock = { + insert, + transaction: async (fn: (tx: unknown) => unknown) => fn({ insert }), + query: { + resident: { findMany: () => Promise.resolve(store.Resident ?? []) }, + placement: { findMany: () => Promise.resolve(store.Placement ?? []) }, + houseRule: { findFirst: () => Promise.resolve({ id: 'org-night-quiet' }) }, }, + // Subquery builder feeding the summary's $count calls. + select: () => ({ from: () => ({ where: () => ({}) }) }), + $count: (table: unknown) => + Promise.resolve( + (store[getTableName(table as Parameters[0])] ?? []).length, + ), } - return { prisma: prisma as unknown as PrismaClient, recorded } + return { db: dbMock as unknown as typeof db, recorded } } beforeEach(() => { @@ -139,15 +152,15 @@ beforeEach(() => { describe('seedDemoData', () => { it('creates the full presentation narrative: 5 units, 15 residents', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.unitCodes).toHaveLength(5) expect(recorded.residentCodes).toHaveLength(15) }) it('gives EVERY unit a demo-prefixed code, so the scoped reset can find it', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) for (const code of recorded.unitCodes) { expect(code).toMatch(new RegExp(`^${DEMO_UNIT_CODE_PREFIX}`)) } @@ -155,8 +168,8 @@ describe('seedDemoData', () => { it('gives EVERY resident a demo-prefixed code or the configured login code', async () => { process.env.DEMO_RESIDENT_CODE = 'RES-CUSTOM' - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) for (const code of recorded.residentCodes) { if (code === 'RES-CUSTOM') continue expect(code).toMatch(new RegExp(`^${DEMO_RESIDENT_CODE_PREFIX}`)) @@ -165,8 +178,8 @@ describe('seedDemoData', () => { }) it('gives EVERY demo resident a name — the narrative already uses one', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) // The incident texts name Alexei and Petro; the resident rows used to carry // no displayName, so the chore board and the queues called the same person @@ -181,18 +194,20 @@ describe('seedDemoData', () => { }) it('assigns the demo login code to a resident (the portal tour identity)', async () => { - const { prisma, recorded } = createPrismaMock() - const summary = await seedDemoData(prisma) + const { db, recorded } = createDbMock() + const summary = await seedDemoData(db) expect(summary.demoResidentCode).toBe(resolveDemoResidentCode()) expect(recorded.residentCodes).toContain(summary.demoResidentCode) }) it('keeps every expense internally consistent: shares sum to the amount', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.expenses.length).toBeGreaterThan(0) for (const expense of recorded.expenses) { - const sum = expense.shares.create.reduce((acc, s) => acc + s.amountRappen, 0) + const sum = recorded.expenseShares + .filter((share) => share.expenseId === expense.id) + .reduce((acc, share) => acc + share.amountRappen, 0) expect(sum).toBe(expense.amountRappen) } }) @@ -200,14 +215,14 @@ describe('seedDemoData', () => { // An empty page reads as a missing feature. These pin the surfaces that // shipped empty and made the tour look like less than the product is. it('fills the chore board, so the tour never shows "Noch keine Aufgaben"', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.taskTitles.length).toBeGreaterThan(0) }) it('fills the maintenance board with both open and finished work', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.maintenanceStatuses).toContain('OPEN') expect(recorded.maintenanceStatuses).toContain('COMPLETED') }) @@ -215,8 +230,8 @@ describe('seedDemoData', () => { it('fills BOTH halves of the marketplace, so the service side is not invisible', async () => { // A demo showing only furniture teaches a visitor that the board handles // objects — which is precisely the belief the service half exists to end. - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) const halves = recorded.marketplaceKinds.map((kind) => natureOfKind(kind as never)) expect({ @@ -228,8 +243,8 @@ describe('seedDemoData', () => { it('seeds an event that has not happened yet AND one that has', async () => { // Only past events means an empty "Kommt" section and no RSVP to press; // only future ones means the "Vorbei" record never appears at all. - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) const now = Date.now() expect({ @@ -243,15 +258,15 @@ describe('seedDemoData', () => { // deletes by demo prefix can never reach one. On an instance sharing a // database with a real flat they would accumulate nightly and show real // residents invented offers with invented phone numbers. - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.activityCategories).toEqual([]) }) it('creates the activity catalogue only when the caller owns the whole database', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma, { siteWideContent: true }) + const { db, recorded } = createDbMock() + await seedDemoData(db, { siteWideContent: true }) // One per category, so every position in the portal's filter row returns // something — a filter landing on "Keine Ergebnisse" reads as broken, not @@ -261,8 +276,8 @@ describe('seedDemoData', () => { }) it('gives the DEMO LOGIN an answered report — a reply to a roommate proves nothing', async () => { - const { prisma, recorded } = createPrismaMock() - const summary = await seedDemoData(prisma) + const { db, recorded } = createDbMock() + const summary = await seedDemoData(db) const demoId = recorded.residentIdsByCode[summary.demoResidentCode] const answered = recorded.maintenance.filter((m) => m.status === 'COMPLETED' && m.resolution) expect(answered.some((m) => m.reportedById === demoId)).toBe(true) @@ -271,22 +286,22 @@ describe('seedDemoData', () => { it('seeds a proposal ALREADY IN VOTING — a fresh one could never reach a vote', async () => { // Voting opens after a 3-day discussion window and the demo world is wiped // nightly, so an un-backdated proposal makes the ballot unreachable forever. - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.proposalStatuses).toContain('VOTING') }) it('seeds a decided proposal and one awaiting staff, so both queues have content', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.proposalStatuses).toContain('ACCEPTED') expect(recorded.proposalStatuses).toContain('NEEDS_STAFF_CONFIRMATION') expect(recorded.proposalStatuses).toContain('DISCUSSION') }) it('adopts a house rule from the accepted proposal, so the two tiers are visible', async () => { - const { prisma, recorded } = createPrismaMock() - await seedDemoData(prisma) + const { db, recorded } = createDbMock() + await seedDemoData(db) expect(recorded.unitRuleTitles.length).toBeGreaterThan(0) }) @@ -297,8 +312,8 @@ describe('seedDemoData', () => { // concluded the pillar was unbuilt. it('gives EVERY demo resident integration evidence', async () => { - const { prisma, recorded } = createPrismaMock() - const summary = await seedDemoData(prisma) + const { db, recorded } = createDbMock() + const summary = await seedDemoData(db) const withEvidence = new Set(recorded.learningResidentIds) const everyResidentId = Object.values(recorded.residentIdsByCode) @@ -313,8 +328,8 @@ describe('seedDemoData', () => { it('assigns the care seats when the deployment has a staff account', async () => { // Without an assignment, "Meine Klient*innen" — the DEFAULT view for every // non-Leitung role — is empty however full the database is. - const { prisma } = createPrismaMock() - const summary = await seedDemoData(prisma, { careStaffId: 'demo-staff' }) + const { db } = createDbMock() + const summary = await seedDemoData(db, { careStaffId: 'demo-staff' }) expect(summary.careAssignments).toBeGreaterThan(0) }) @@ -325,8 +340,8 @@ describe('seedDemoData', () => { // a visitor meets four empty care panels — the same failure the chore and // proposal seeds exist to prevent. COMPLETED shows what the feature // produces; SCHEDULED is the one the visitor gets to close themselves. - const { prisma, recorded } = createPrismaMock() - const summary = await seedDemoData(prisma, { careStaffId: 'demo-staff' }) + const { db, recorded } = createDbMock() + const summary = await seedDemoData(db, { careStaffId: 'demo-staff' }) expect(summary.appointments).toBeGreaterThan(0) expect(recorded.appointmentStatuses).toContain('COMPLETED') @@ -334,8 +349,8 @@ describe('seedDemoData', () => { }) it('seeds no appointments without a staff account to hold them', async () => { - const { prisma, recorded } = createPrismaMock() - const summary = await seedDemoData(prisma) + const { db, recorded } = createDbMock() + const summary = await seedDemoData(db) expect(summary.appointments).toBe(0) expect(recorded.appointmentStatuses).toHaveLength(0) @@ -344,8 +359,8 @@ describe('seedDemoData', () => { it('invents no colleague when the deployment has no demo staff door', async () => { // A fake staff row would appear in every real "zuständig" picker on an // instance that also holds real data. - const { prisma } = createPrismaMock() - const summary = await seedDemoData(prisma) + const { db } = createDbMock() + const summary = await seedDemoData(db) expect(summary.careAssignments).toBe(0) expect(summary.learningRecords).toBeGreaterThan(0) diff --git a/src/lib/demo/seed-data.ts b/src/lib/demo/seed-data.ts index 85cc1e69..092b881a 100644 --- a/src/lib/demo/seed-data.ts +++ b/src/lib/demo/seed-data.ts @@ -1385,15 +1385,13 @@ export async function seedDemoData( date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), }) .returning() - await tx - .insert(expenseShare) - .values( - unit5MemberIds.map((residentId) => ({ - expenseId: created.id, - residentId, - amountRappen: 1200, - })), - ) + await tx.insert(expenseShare).values( + unit5MemberIds.map((residentId) => ({ + expenseId: created.id, + residentId, + amountRappen: 1200, + })), + ) return created }) await dbClient.transaction(async (tx) => { @@ -1409,15 +1407,13 @@ export async function seedDemoData( date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), }) .returning() - await tx - .insert(expenseShare) - .values( - unit5MemberIds.map((residentId) => ({ - expenseId: created.id, - residentId, - amountRappen: 465, - })), - ) + await tx.insert(expenseShare).values( + unit5MemberIds.map((residentId) => ({ + expenseId: created.id, + residentId, + amountRappen: 465, + })), + ) }) await dbClient.insert(settlement).values({ housingUnitId: unit5.id, diff --git a/src/lib/governance/__tests__/lifecycle.test.ts b/src/lib/governance/__tests__/lifecycle.test.ts index 354ab3c9..59625de7 100644 --- a/src/lib/governance/__tests__/lifecycle.test.ts +++ b/src/lib/governance/__tests__/lifecycle.test.ts @@ -8,36 +8,69 @@ * when the house actually said yes. */ -import { prisma } from '@/lib/db' import { adoptProposal, advanceDueProposals, closeProposal, expireStaleAgreements, } from '../lifecycle' - -jest.mock('@/lib/db', () => ({ - prisma: { - proposal: { findMany: jest.fn(), findUnique: jest.fn(), updateMany: jest.fn() }, - houseRule: { create: jest.fn(), update: jest.fn() }, - placement: { findMany: jest.fn() }, - conflictAgreement: { updateMany: jest.fn() }, - }, -})) +import { conflictAgreement } from '@/lib/db' +import { and, inArray, lt } from 'drizzle-orm' +import { whereParts } from '@/test-utils/drizzle-where' + +const mockProposalFindFirst = jest.fn() +const mockProposalFindMany = jest.fn() +const mockPlacementFindMany = jest.fn() +// insert(houseRule).values(v) → (v) +const mockRuleCreate = jest.fn() +// update(houseRule).set(d).where(w) → (d, whereParts) +const mockRuleUpdate = jest.fn() +// The guarded proposal update — returns the rows `.returning()` yields, so a +// test can make it lose the close race by returning []. +const mockProposalUpdate = jest.fn() +// update(conflictAgreement) — recorded with the RAW where tree for comparison. +const mockAgreementUpdate = jest.fn() + +jest.mock('@/lib/db', () => { + const actual = jest.requireActual('@/lib/db') + const { whereParts: parts } = jest.requireActual( + '@/test-utils/drizzle-where', + ) + return { + ...actual, + db: { + query: { + proposal: { + findFirst: (...a: unknown[]) => mockProposalFindFirst(...a), + findMany: (...a: unknown[]) => mockProposalFindMany(...a), + }, + placement: { findMany: (...a: unknown[]) => mockPlacementFindMany(...a) }, + }, + insert: () => ({ values: (v: unknown) => Promise.resolve(mockRuleCreate(v)) }), + update: (table: unknown) => ({ + set: (data: unknown) => ({ + where: (w: unknown) => { + if (table === actual.houseRule) { + return Promise.resolve(mockRuleUpdate(data, parts(w))) + } + const rows = + table === actual.conflictAgreement + ? mockAgreementUpdate(data, w) + : mockProposalUpdate(data, parts(w)) + return Object.assign(Promise.resolve(rows), { + returning: () => Promise.resolve(rows), + }) + }, + }), + }), + }, + } +}) jest.mock('@/lib/logger', () => ({ logger: { errorWithCause: jest.fn(), info: jest.fn(), warn: jest.fn() }, })) -const mockProposal = prisma.proposal as unknown as { - findMany: jest.Mock - findUnique: jest.Mock - updateMany: jest.Mock -} -const mockRule = prisma.houseRule as unknown as { create: jest.Mock; update: jest.Mock } -const mockPlacement = prisma.placement as unknown as { findMany: jest.Mock } -const mockAgreement = prisma.conflictAgreement as unknown as { updateMany: jest.Mock } - const ORG_TOPIC = { id: 'org-kitchen', scope: 'ORG', @@ -71,119 +104,114 @@ function votingProposal(overrides: Record = {}) { beforeEach(() => { jest.clearAllMocks() - mockProposal.updateMany.mockResolvedValue({ count: 1 }) - mockProposal.findMany.mockResolvedValue([]) - mockRule.create.mockResolvedValue({ id: 'new-rule' }) - mockRule.update.mockResolvedValue({}) - mockAgreement.updateMany.mockResolvedValue({ count: 0 }) + mockProposalUpdate.mockReturnValue([{ id: 'p1' }]) + mockProposalFindMany.mockResolvedValue([]) + mockRuleCreate.mockResolvedValue({ id: 'new-rule' }) + mockRuleUpdate.mockResolvedValue({}) + mockAgreementUpdate.mockReturnValue([]) }) describe('closeProposal', () => { it('accepts a passing vote and creates the house rule', async () => { - mockProposal.findUnique.mockResolvedValue(votingProposal()) + mockProposalFindFirst.mockResolvedValue(votingProposal()) const status = await closeProposal('p1') expect(status).toBe('ACCEPTED') - expect(mockRule.create).toHaveBeenCalledWith( + expect(mockRuleCreate).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.objectContaining({ - scope: 'UNIT', - housingUnitId: 'unit-1', - parentRuleId: ORG_TOPIC.id, - adoptedByProposalId: 'p1', - }), + scope: 'UNIT', + housingUnitId: 'unit-1', + parentRuleId: ORG_TOPIC.id, + adoptedByProposalId: 'p1', }), ) }) it('records the plain-language explanation as the outcome', async () => { - mockProposal.findUnique.mockResolvedValue(votingProposal()) + mockProposalFindFirst.mockResolvedValue(votingProposal()) await closeProposal('p1') - const update = mockProposal.updateMany.mock.calls[0][0] - expect(update.data.outcomeSummary).toContain('Angenommen') - expect(update.data.outcomeSummary).toContain('3 dafür') + const [data] = mockProposalUpdate.mock.calls[0] + expect(data.outcomeSummary).toContain('Angenommen') + expect(data.outcomeSummary).toContain('3 dafür') }) it('creates no rule when the house voted it down', async () => { - mockProposal.findUnique.mockResolvedValue( + mockProposalFindFirst.mockResolvedValue( votingProposal({ votes: [{ choice: 'NO' }, { choice: 'NO' }, { choice: 'YES' }] }), ) const status = await closeProposal('p1') expect(status).toBe('REJECTED') - expect(mockRule.create).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() }) it('expires rather than rejects when quorum was never reached', async () => { // A house that did not vote has not said no — it can try again. - mockProposal.findUnique.mockResolvedValue(votingProposal({ votes: [{ choice: 'YES' }] })) + mockProposalFindFirst.mockResolvedValue(votingProposal({ votes: [{ choice: 'YES' }] })) expect(await closeProposal('p1')).toBe('EXPIRED') - expect(mockRule.create).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() }) it('holds an advisory decision for staff instead of adopting it', async () => { - mockProposal.findUnique.mockResolvedValue(votingProposal({ decisionMode: 'RESIDENT_ADVISORY' })) + mockProposalFindFirst.mockResolvedValue(votingProposal({ decisionMode: 'RESIDENT_ADVISORY' })) expect(await closeProposal('p1')).toBe('NEEDS_STAFF_CONFIRMATION') - expect(mockRule.create).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() }) it('holds a decision that claims to strengthen an AOZ rule', async () => { - mockProposal.findUnique.mockResolvedValue( + mockProposalFindFirst.mockResolvedValue( votingProposal({ parentOrgRule: { ...ORG_TOPIC, delegation: 'UNIT_MAY_STRENGTHEN' }, }), ) expect(await closeProposal('p1')).toBe('NEEDS_STAFF_CONFIRMATION') - expect(mockRule.create).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() }) it('does nothing to a proposal that is not open for voting', async () => { - mockProposal.findUnique.mockResolvedValue(votingProposal({ status: 'ACCEPTED' })) + mockProposalFindFirst.mockResolvedValue(votingProposal({ status: 'ACCEPTED' })) expect(await closeProposal('p1')).toBeNull() - expect(mockProposal.updateMany).not.toHaveBeenCalled() + expect(mockProposalUpdate).not.toHaveBeenCalled() }) it('cannot adopt twice when two callers close the same proposal at once', async () => { // The guarded update loses the race and must not create a second rule. - mockProposal.findUnique.mockResolvedValue(votingProposal()) - mockProposal.updateMany.mockResolvedValue({ count: 0 }) + mockProposalFindFirst.mockResolvedValue(votingProposal()) + mockProposalUpdate.mockReturnValue([]) expect(await closeProposal('p1')).toBeNull() - expect(mockRule.create).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() }) it('returns null for a proposal that no longer exists', async () => { - mockProposal.findUnique.mockResolvedValue(null) + mockProposalFindFirst.mockResolvedValue(null) expect(await closeProposal('p1')).toBeNull() }) }) describe('adoptProposal', () => { it('archives the target rule on a repeal', async () => { - mockProposal.findUnique.mockResolvedValue( + mockProposalFindFirst.mockResolvedValue( votingProposal({ type: 'REPEAL_RULE', targetRuleId: 'rule-9' }), ) await adoptProposal('p1') - expect(mockRule.update).toHaveBeenCalledWith( - expect.objectContaining({ - where: { id: 'rule-9' }, - data: expect.objectContaining({ status: 'ARCHIVED' }), - }), - ) + expect(mockRuleUpdate).toHaveBeenCalledWith(expect.objectContaining({ status: 'ARCHIVED' }), { + id: 'rule-9', + }) }) it('bumps the version on an amendment so everyone re-acknowledges', async () => { - mockProposal.findUnique.mockResolvedValue( + mockProposalFindFirst.mockResolvedValue( votingProposal({ type: 'AMEND_RULE', targetRule: { id: 'rule-9', version: 3 }, @@ -193,18 +221,19 @@ describe('adoptProposal', () => { await adoptProposal('p1') - expect(mockRule.update).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ version: 4 }) }), + expect(mockRuleUpdate).toHaveBeenCalledWith( + expect.objectContaining({ version: 4 }), + expect.anything(), ) }) it('records a one-off house decision without creating a standing rule', async () => { - mockProposal.findUnique.mockResolvedValue(votingProposal({ type: 'HOUSE_DECISION' })) + mockProposalFindFirst.mockResolvedValue(votingProposal({ type: 'HOUSE_DECISION' })) await adoptProposal('p1') - expect(mockRule.create).not.toHaveBeenCalled() - expect(mockRule.update).not.toHaveBeenCalled() + expect(mockRuleCreate).not.toHaveBeenCalled() + expect(mockRuleUpdate).not.toHaveBeenCalled() }) }) @@ -212,10 +241,10 @@ describe('advanceDueProposals', () => { it('snapshots the electorate when voting opens, not when the proposal was written', async () => { // People move in and out; a quorum measured against last week's roster is // not a quorum. - mockProposal.findMany + mockProposalFindMany .mockResolvedValueOnce([{ id: 'p1', housingUnitId: 'unit-1' }]) // due to open .mockResolvedValueOnce([]) // due to close - mockPlacement.findMany.mockResolvedValue([ + mockPlacementFindMany.mockResolvedValue([ { residentId: 'r1' }, { residentId: 'r2' }, { residentId: 'r2' }, // duplicate placement must not inflate the electorate @@ -225,16 +254,15 @@ describe('advanceDueProposals', () => { const result = await advanceDueProposals(new Date('2026-03-10T00:00:00Z')) expect(result.opened).toBe(1) - expect(mockProposal.updateMany).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ status: 'VOTING', eligibleVoterCount: 3 }), - }), + expect(mockProposalUpdate).toHaveBeenCalledWith( + expect.objectContaining({ status: 'VOTING', eligibleVoterCount: 3 }), + expect.anything(), ) }) it('closes proposals whose voting window has elapsed', async () => { - mockProposal.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([{ id: 'p1' }]) - mockProposal.findUnique.mockResolvedValue(votingProposal()) + mockProposalFindMany.mockResolvedValueOnce([]).mockResolvedValueOnce([{ id: 'p1' }]) + mockProposalFindFirst.mockResolvedValue(votingProposal()) const result = await advanceDueProposals(new Date()) @@ -242,38 +270,44 @@ describe('advanceDueProposals', () => { }) it('never throws — it runs inside page loads', async () => { - mockProposal.findMany.mockRejectedValue(new Error('db down')) + mockProposalFindMany.mockRejectedValue(new Error('db down')) await expect(advanceDueProposals(new Date())).resolves.toEqual({ opened: 0, closed: 0 }) }) it('scopes to one unit when asked', async () => { - mockProposal.findMany.mockResolvedValue([]) + mockProposalFindMany.mockResolvedValue([]) await advanceDueProposals(new Date(), 'unit-7') - expect(mockProposal.findMany).toHaveBeenCalledWith( - expect.objectContaining({ where: expect.objectContaining({ housingUnitId: 'unit-7' }) }), - ) + expect(whereParts(mockProposalFindMany.mock.calls[0][0].where)).toMatchObject({ + housingUnitId: 'unit-7', + }) }) }) describe('expireStaleAgreements', () => { it('expires agreements nobody reviewed, so silence never counts as success', async () => { - mockAgreement.updateMany.mockResolvedValue({ count: 2 }) + mockAgreementUpdate.mockReturnValue([{ id: 'a1' }, { id: 'a2' }]) const count = await expireStaleAgreements(new Date('2026-03-20T00:00:00Z'), 7) expect(count).toBe(2) - const args = mockAgreement.updateMany.mock.calls[0][0] - expect(args.where.status).toEqual({ in: ['PROPOSED', 'ACCEPTED'] }) - expect(args.data).toEqual({ status: 'EXPIRED' }) + // Compared against the REAL drizzle expression — same statuses, same + // cutoff, without hand-parsing the SQL tree. + expect(mockAgreementUpdate).toHaveBeenCalledWith( + { status: 'EXPIRED' }, + and( + inArray(conflictAgreement.status, ['PROPOSED', 'ACCEPTED']), + lt(conflictAgreement.reviewDate, new Date('2026-03-13T00:00:00Z')), + ), + ) }) it('leaves agreements still inside their grace period alone', async () => { await expireStaleAgreements(new Date('2026-03-20T00:00:00Z'), 7) - const cutoff = mockAgreement.updateMany.mock.calls[0][0].where.reviewDate.lt as Date + const cutoff = whereParts(mockAgreementUpdate.mock.calls[0][1]).reviewDate as Date expect(cutoff.toISOString().slice(0, 10)).toBe('2026-03-13') }) }) diff --git a/src/lib/governance/__tests__/sync-org-rules.test.ts b/src/lib/governance/__tests__/sync-org-rules.test.ts index d12ee0da..91b54b46 100644 --- a/src/lib/governance/__tests__/sync-org-rules.test.ts +++ b/src/lib/governance/__tests__/sync-org-rules.test.ts @@ -1,33 +1,43 @@ import { syncOrgRules } from '../sync-org-rules' import { ORG_RULE_CATALOG } from '@/lib/config/house-rules' -import type { PrismaClient } from '@prisma/client' +import { whereParts } from '@/test-utils/drizzle-where' +import type { db } from '@/lib/db' /** * The catalog sync runs unattended on every cron tick, so the properties that * matter are: it must not duplicate, it must not reset acknowledgements for * rules that did not change, and it MUST invalidate them for rules that did. */ -function makePrisma(existing: Record = {}) { +function makeDb(existing: Record = {}) { const created: unknown[] = [] const updated: { where: unknown; data: Record }[] = [] - const prisma = { - houseRule: { - findUnique: jest.fn( - async ({ where }: { where: { key: string } }) => existing[where.key] ?? null, - ), - create: jest.fn(async ({ data }: { data: unknown }) => { + const dbMock = { + query: { + houseRule: { + // Looked up by `eq(houseRule.key, …)` — the key is read back out of + // the where-expression, same dispatch the Prisma mock did on `where.key`. + findFirst: jest.fn( + async ({ where }: { where: unknown }) => + existing[whereParts(where).key as string] ?? null, + ), + }, + }, + insert: () => ({ + values: jest.fn(async (data: unknown) => { created.push(data) - return data }), - update: jest.fn(async (args: { where: unknown; data: Record }) => { - updated.push(args) - return args.data + }), + update: () => ({ + set: (data: Record) => ({ + where: async (where: unknown) => { + updated.push({ where: whereParts(where), data }) + }, }), - }, - } as unknown as PrismaClient + }), + } - return { prisma, created, updated } + return { db: dbMock as unknown as typeof db, created, updated } } function seededRule(key: string, overrides: Record = {}) { @@ -47,9 +57,9 @@ function seededRule(key: string, overrides: Record = {}) { describe('syncOrgRules', () => { it('creates the whole catalog on an empty database', async () => { - const { prisma, created } = makePrisma() + const { db, created } = makeDb() - const result = await syncOrgRules(prisma) + const result = await syncOrgRules(db) expect(result.created).toBe(ORG_RULE_CATALOG.length) expect(result.amended).toBe(0) @@ -59,9 +69,9 @@ describe('syncOrgRules', () => { it('is idempotent — a second run changes nothing', async () => { const existing = Object.fromEntries(ORG_RULE_CATALOG.map((r) => [r.key, seededRule(r.key)])) - const { prisma, created, updated } = makePrisma(existing) + const { db, created, updated } = makeDb(existing) - const result = await syncOrgRules(prisma) + const result = await syncOrgRules(db) expect(result.created).toBe(0) expect(result.amended).toBe(0) @@ -78,9 +88,9 @@ describe('syncOrgRules', () => { seededRule(r.key, r.key === key ? { body: 'Alter Text, der ersetzt wird.' } : {}), ]), ) - const { prisma, updated } = makePrisma(existing) + const { db, updated } = makeDb(existing) - const result = await syncOrgRules(prisma) + const result = await syncOrgRules(db) expect(result.amended).toBe(1) expect(result.amendedKeys).toEqual([key]) @@ -98,9 +108,9 @@ describe('syncOrgRules', () => { seededRule(r.key, r.key === key ? { category: 'OTHER' } : {}), ]), ) - const { prisma, updated } = makePrisma(existing) + const { db, updated } = makeDb(existing) - const result = await syncOrgRules(prisma) + const result = await syncOrgRules(db) expect(result.amended).toBe(0) expect(updated).toHaveLength(1) @@ -115,9 +125,9 @@ describe('syncOrgRules', () => { seededRule(r.key, r.key === key ? { status: 'ARCHIVED' } : {}), ]), ) - const { prisma, updated } = makePrisma(existing) + const { db, updated } = makeDb(existing) - await syncOrgRules(prisma) + await syncOrgRules(db) expect(updated).toHaveLength(1) expect(updated[0].data.status).toBe('ACTIVE') diff --git a/src/lib/governance/__tests__/voting.test.ts b/src/lib/governance/__tests__/voting.test.ts index d8c5ca9f..ad6cf135 100644 --- a/src/lib/governance/__tests__/voting.test.ts +++ b/src/lib/governance/__tests__/voting.test.ts @@ -1,5 +1,5 @@ import { tallyVotes, canHoldVote, computeSchedule, buildPolicySnapshot } from '../voting' -import type { VoteChoice } from '@prisma/client' +import type { VoteChoice } from '@/lib/db' import { DECISION_TIMING, THRESHOLD_APPROVAL_PERCENT } from '@/lib/config/decisions' function votes(spec: Partial>) { diff --git a/src/lib/opportunities/__tests__/opportunity-kinds.test.ts b/src/lib/opportunities/__tests__/opportunity-kinds.test.ts index d24d8898..32566c95 100644 --- a/src/lib/opportunities/__tests__/opportunity-kinds.test.ts +++ b/src/lib/opportunities/__tests__/opportunity-kinds.test.ts @@ -4,18 +4,18 @@ * 1. `evidenceForStartedApplication` passes `opportunity.kind` straight into a * LearningRecord. That is only sound while every OpportunityKind is also a * LearningKind. Add `INTERNSHIP` to one side in the work phase and forget - * the other, and the failure surfaces as a Prisma enum error at the single - * moment a coach is recording real work — the worst possible time. + * the other, and the failure surfaces as a database enum error at the + * single moment a coach is recording real work — the worst possible time. * - * 2. A config array and a Prisma enum are two spellings of one fact. Nothing - * else in this repo compares them, so a value added to `config/` and not to - * `schema.prisma` type-checks, renders a form option, and fails only on - * save. This reads the schema file itself rather than a hand-copied list — - * a fixture would freeze the very drift it is meant to catch. + * 2. A config array and a database enum are two spellings of one fact. + * Nothing else in this repo compares them, so a value added to `config/` + * and not to the drizzle schema type-checks, renders a form option, and + * fails only on save. This reads the schema's pgEnum itself rather than a + * hand-copied list — a fixture would freeze the very drift it is meant to + * catch. */ -import { readFileSync } from 'fs' -import { join } from 'path' +import * as dbSchema from '@/lib/db/schema' import { APPLICATION_STAGES, OPPORTUNITY_KINDS, @@ -24,17 +24,17 @@ import { } from '@/lib/config/opportunities' import { LEARNING_KINDS } from '@/lib/config/learning' -const SCHEMA_PATH = join(process.cwd(), 'prisma', 'schema.prisma') - -function enumValuesFromSchema(name: string): string[] { - const schema = readFileSync(SCHEMA_PATH, 'utf8') - const match = schema.match(new RegExp(`enum ${name} \\{([^}]*)\\}`)) - if (!match) throw new Error(`enum ${name} not found in prisma/schema.prisma`) - - return match[1] - .split('\n') - .map((line) => line.replace(/\/\/.*$/, '').trim()) - .filter((line) => line.length > 0) +function enumValuesFromDb(name: string): string[] { + for (const exported of Object.values(dbSchema)) { + if ( + typeof exported === 'function' && + 'enumName' in exported && + (exported as { enumName: string }).enumName === name + ) { + return [...(exported as unknown as { enumValues: string[] }).enumValues] + } + } + throw new Error(`enum ${name} not found in the drizzle schema`) } describe('OpportunityKind is a subset of LearningKind', () => { @@ -51,13 +51,13 @@ describe('config matches the database enums', () => { ['ApplicationStage', APPLICATION_STAGES], ])('%s', (enumName, configValues) => { // Sorted: declaration order is a display concern and differs legitimately. - expect([...configValues].sort()).toEqual(enumValuesFromSchema(enumName as string).sort()) + expect([...configValues].sort()).toEqual(enumValuesFromDb(enumName as string).sort()) }) it('actually reads the schema, and fails loudly when it cannot', () => { // Without this, a rename upstream turns every check above into a silent // pass over an empty list — the failed-fetch-as-fact trap. - expect(enumValuesFromSchema('LearningKind').length).toBeGreaterThan(0) - expect(() => enumValuesFromSchema('NoSuchEnum')).toThrow(/not found/) + expect(enumValuesFromDb('LearningKind').length).toBeGreaterThan(0) + expect(() => enumValuesFromDb('NoSuchEnum')).toThrow(/not found/) }) }) diff --git a/src/lib/opportunities/__tests__/work-permit-gate.test.ts b/src/lib/opportunities/__tests__/work-permit-gate.test.ts index 8659a086..91c61c7e 100644 --- a/src/lib/opportunities/__tests__/work-permit-gate.test.ts +++ b/src/lib/opportunities/__tests__/work-permit-gate.test.ts @@ -26,17 +26,28 @@ import { permitRequirementIsStated, } from '@/lib/config/opportunities' import { OpportunityInputSchema } from '@/lib/validation' -import { prisma } from '@/lib/db' import { publishOpportunity } from '@/lib/actions/opportunities' +import { whereParts as mockWhereParts } from '@/test-utils/drizzle-where' // --- the action path ------------------------------------------------------- +const mockOpportunityFindFirst = jest.fn() +// (set, whereParts) → the rows `.returning()` yields +const mockOpportunityUpdate = jest.fn() + jest.mock('@/lib/db', () => ({ - prisma: { - opportunity: { - findUnique: jest.fn(), - update: jest.fn(), + ...jest.requireActual('@/lib/db'), + db: { + query: { + opportunity: { findFirst: (...a: unknown[]) => mockOpportunityFindFirst(...a) }, }, + update: () => ({ + set: (data: unknown) => ({ + where: (w: unknown) => ({ + returning: () => Promise.resolve(mockOpportunityUpdate(data, mockWhereParts(w))), + }), + }), + }), }, })) jest.mock('next/cache', () => ({ revalidatePath: jest.fn() })) @@ -138,49 +149,43 @@ describe('publishing through the form', () => { }) describe('publishing through the button that skips the form', () => { - const mockPrisma = prisma as unknown as { - opportunity: { findUnique: jest.Mock; update: jest.Mock } - } - beforeEach(() => { jest.clearAllMocks() - mockPrisma.opportunity.update.mockResolvedValue({}) + mockOpportunityUpdate.mockReturnValue([{ id: 'opp-1' }]) }) it.each([...WORK_OPPORTUNITY_KINDS])( 'refuses to publish a stored %s draft that still says NONE', async (kind) => { - mockPrisma.opportunity.findUnique.mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ kind, permitRequirement: 'NONE', }) await expect(publishOpportunity('opp-1')).rejects.toThrow(/Bewilligungsweg/) - expect(mockPrisma.opportunity.update).not.toHaveBeenCalled() + expect(mockOpportunityUpdate).not.toHaveBeenCalled() }, ) it('publishes a work listing once a route is stated', async () => { - mockPrisma.opportunity.findUnique.mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ kind: 'EMPLOYMENT', permitRequirement: 'PERMIT_REQUIRED', }) await publishOpportunity('opp-1') - expect(mockPrisma.opportunity.update).toHaveBeenCalledWith( - expect.objectContaining({ where: { id: 'opp-1' } }), - ) + expect(mockOpportunityUpdate).toHaveBeenCalledWith(expect.anything(), { id: 'opp-1' }) }) it('leaves unpaid listings publishable', async () => { - mockPrisma.opportunity.findUnique.mockResolvedValue({ + mockOpportunityFindFirst.mockResolvedValue({ kind: 'VOLUNTEERING', permitRequirement: 'NONE', }) await publishOpportunity('opp-1') - expect(mockPrisma.opportunity.update).toHaveBeenCalled() + expect(mockOpportunityUpdate).toHaveBeenCalled() }) }) diff --git a/src/lib/privacy/__tests__/profile-visibility.test.ts b/src/lib/privacy/__tests__/profile-visibility.test.ts index 24e0744d..73591315 100644 --- a/src/lib/privacy/__tests__/profile-visibility.test.ts +++ b/src/lib/privacy/__tests__/profile-visibility.test.ts @@ -4,7 +4,7 @@ import { PROFILE_VISIBILITY_OPTIONS, type ProfileViewer, } from '@/lib/privacy/profile-visibility' -import type { ProfileVisibility } from '@prisma/client' +import type { ProfileVisibility } from '@/lib/db' /** * The failure this suite exists to prevent is not a crash. It is a photo shown diff --git a/src/lib/seed/__tests__/integration-evidence.test.ts b/src/lib/seed/__tests__/integration-evidence.test.ts index f8e51942..fa1d5212 100644 --- a/src/lib/seed/__tests__/integration-evidence.test.ts +++ b/src/lib/seed/__tests__/integration-evidence.test.ts @@ -9,6 +9,8 @@ * and that panel is the first thing a Jobcoach looks at. */ +import { getTableName } from 'drizzle-orm' +import { appointment, satisfactionCheckIn } from '@/lib/db' import { evidenceForResident, seedIntegrationEvidence } from '../integration-evidence' import { LEARNING_PULSE_WINDOW_DAYS } from '../../config/learning' @@ -172,43 +174,65 @@ describe('evidenceForResident', () => { * was invisible here. */ describe('seeded appointments', () => { - function makePrisma() { + function makeDb() { const created: Record[]> = { appointment: [], checkIn: [] } - return { - created, - client: { + // A drizzle-shaped stand-in for the surface seedIntegrationEvidence uses: + // query.resident/placement.findMany, and insert(table).values(v) awaited + // bare, with .returning() (held appointment) and .onConflictDoNothing() + // (care seats). Dispatch is on the REAL table identity, so the assertions + // keep the per-table discrimination the Prisma model names carried. + const record = (table: unknown, v: unknown) => { + const name: string = getTableName(table as typeof appointment) + if (name === getTableName(appointment)) { + created.appointment.push(v as Record) + } + if (name === getTableName(satisfactionCheckIn)) { + created.checkIn.push(v as Record) + } + } + const client = { + query: { resident: { findMany: jest.fn(async () => [ { id: 'r1', languages: ['German'], ageRange: 'ADULT', choresContribution: 4 }, { id: 'r2', languages: ['Tigrinya'], ageRange: 'ADULT', choresContribution: 2 }, ]), }, - learningRecord: { createMany: jest.fn(async () => ({ count: 0 })) }, - careAssignment: { createMany: jest.fn(async () => ({ count: 8 })) }, placement: { // Only r1 is placed, so only r1 can carry a reading. findMany: jest.fn(async () => [ { id: 'p1', residentId: 'r1', startDate: new Date('2026-01-01') }, ]), }, - appointment: { - create: jest.fn(async (args: { data: Record }) => { - created.appointment.push(args.data) - return { id: `appt-${created.appointment.length}`, ...args.data } - }), - }, - satisfactionCheckIn: { - create: jest.fn(async (args: { data: Record }) => { - created.checkIn.push(args.data) - return { id: 'ci-1' } - }), - }, }, + insert: jest.fn((table: unknown) => ({ + values: (v: Record | Record[]) => ({ + then: ( + resolve: (x: { rowCount: number }) => unknown, + reject?: (e: unknown) => unknown, + ) => { + record(table, v) + return Promise.resolve({ rowCount: Array.isArray(v) ? v.length : 1 }).then( + resolve, + reject, + ) + }, + onConflictDoNothing: () => { + record(table, v) + return Promise.resolve({ rowCount: 8 }) + }, + returning: async () => { + record(table, v) + return [{ id: `appt-${created.appointment.length}`, ...(v as Record) }] + }, + }), + })), } + return { created, client } } it('creates no appointments when the deployment has no staff account', async () => { - const { client, created } = makePrisma() + const { client, created } = makeDb() // Same rule the care seats follow: with nobody to hold the appointment, // inventing a colleague would put a fake name in a real "zuständig" picker. @@ -223,7 +247,7 @@ describe('seeded appointments', () => { }) it('seeds both states, so the feature is visible AND touchable', async () => { - const { client, created } = makePrisma() + const { client, created } = makeDb() await seedIntegrationEvidence(client as never, { residentIds: ['r1', 'r2'], @@ -238,7 +262,7 @@ describe('seeded appointments', () => { }) it('puts the scheduled one in the future and the held one in the past', async () => { - const { client, created } = makePrisma() + const { client, created } = makeDb() const now = Date.now() await seedIntegrationEvidence(client as never, { @@ -259,7 +283,7 @@ describe('seeded appointments', () => { }) it('attaches every seeded reading to its appointment and to the account', async () => { - const { client, created } = makePrisma() + const { client, created } = makeDb() await seedIntegrationEvidence(client as never, { residentIds: ['r1', 'r2'], @@ -280,7 +304,7 @@ describe('seeded appointments', () => { it('skips the held appointment for a resident with no active placement', async () => { // A check-in hangs off a placement. r2 has none, so it gets the scheduled // appointment only — never a reading with nothing to attach to. - const { client, created } = makePrisma() + const { client, created } = makeDb() await seedIntegrationEvidence(client as never, { residentIds: ['r1', 'r2'], diff --git a/src/lib/validation/schemas.ts b/src/lib/validation/schemas.ts index 645907ca..df088ee8 100644 --- a/src/lib/validation/schemas.ts +++ b/src/lib/validation/schemas.ts @@ -644,11 +644,7 @@ import { TASK_CATEGORY_LABELS, TASK_PRIORITY_LABELS, } from '@/lib/config/household-tasks' -import type { - HouseholdTaskType, - HouseholdTaskCategory, - HouseholdTaskPriority, -} from '@/lib/db' +import type { HouseholdTaskType, HouseholdTaskCategory, HouseholdTaskPriority } from '@/lib/db' export const HouseholdTaskTypeSchema = enumFromKeys(TASK_TYPE_LABELS) export const HouseholdTaskCategorySchema = enumFromKeys(TASK_CATEGORY_LABELS) diff --git a/src/lib/vulnerability/__tests__/vulnerability.test.ts b/src/lib/vulnerability/__tests__/vulnerability.test.ts index a9f82b3f..1822df7b 100644 --- a/src/lib/vulnerability/__tests__/vulnerability.test.ts +++ b/src/lib/vulnerability/__tests__/vulnerability.test.ts @@ -180,10 +180,7 @@ describe('the line this module must not cross', () => { // will drift from the seven columns it summarises, and the reportable // figure and the person's record will disagree with no way to tell which // is right. - const schema = fs.readFileSync( - path.join(__dirname, '..', '..', '..', '..', 'prisma', 'schema.prisma'), - 'utf8', - ) - expect(schema).not.toMatch(/^\s*(isVulnerable|vulnerable|vulnerabilityType)\s/m) + const schema = fs.readFileSync(path.join(__dirname, '..', '..', 'db', 'schema.ts'), 'utf8') + expect(schema).not.toMatch(/^\s*(isVulnerable|vulnerable|vulnerabilityType)\s*:/m) }) }) diff --git a/src/test-utils/drizzle-where.ts b/src/test-utils/drizzle-where.ts new file mode 100644 index 00000000..4063e98e --- /dev/null +++ b/src/test-utils/drizzle-where.ts @@ -0,0 +1,59 @@ +/** + * Introspection helpers for drizzle where-expressions in unit tests. + * + * Prisma mocks could dispatch on plain `where` objects (`{ code: 'AOZ-1' }`); + * drizzle where-args are opaque SQL trees. These helpers pull the + * `column = value` pairs back out of eq()/and(eq(), …) expressions so mocks + * keep the same discriminating power without asserting on internal tree + * shapes. (A Column chunk carries `name` + `table`; a Param chunk carries + * `value` + `encoder`.) + */ + +interface SqlChunkNode { + queryChunks?: unknown[] +} + +/** All `column = value` pairs in an eq()/and(eq(), …) expression. */ +export function whereParts(where: unknown): Record { + const pairs: Record = {} + let column: string | undefined + const walk = (node: unknown): void => { + for (const chunk of (node as SqlChunkNode)?.queryChunks ?? []) { + if (!chunk || typeof chunk !== 'object') continue + if ('queryChunks' in chunk) walk(chunk) + else if ('name' in chunk && 'table' in chunk) column = (chunk as { name: string }).name + else if ('encoder' in chunk && column !== undefined) { + pairs[column] = (chunk as unknown as { value: unknown }).value + column = undefined + } + } + } + walk(where) + return pairs +} + +/** The single `column = value` pair of a bare eq() expression. */ +export function eqParts(where: unknown): { column?: string; value?: unknown } { + const entries = Object.entries(whereParts(where)) + return entries.length ? { column: entries[0][0], value: entries[0][1] } : {} +} + +/** + * The literal text of a sql`` / sql.raw() query — its StringChunk pieces + * joined. Parameters and columns are omitted; enough to tell one raw + * statement from another (e.g. the pg_tables SELECT from the TRUNCATE). + */ +export function sqlText(query: unknown): string { + let text = '' + const walk = (node: unknown): void => { + for (const chunk of (node as SqlChunkNode)?.queryChunks ?? []) { + if (!chunk || typeof chunk !== 'object') continue + if ('queryChunks' in chunk) walk(chunk) + else if ('value' in chunk && Array.isArray((chunk as { value: unknown }).value)) { + text += ((chunk as { value: string[] }).value ?? []).join('') + } + } + } + walk(query) + return text +} diff --git a/tests/maintenance.spec.ts b/tests/maintenance.spec.ts index 96a7e35c..b4b5df91 100644 --- a/tests/maintenance.spec.ts +++ b/tests/maintenance.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { findAdminNavLink, expectNotFoundPage } from './helpers' // storageState from playwright.config handles staff auth -// Seed data (prisma/seed.ts) provides 4 maintenance requests: +// Seed data (scripts/db/seed.ts) provides 4 maintenance requests: // - "Dusche tropft" → OPEN (ZH-006) // - "Heizung Zimmer 2 defekt" → ASSIGNED (ZH-002) // - "Steckdose funktioniert nicht" → COMPLETED (ZH-003) diff --git a/tests/matching-flow.spec.ts b/tests/matching-flow.spec.ts index 342c3770..71d5def1 100644 --- a/tests/matching-flow.spec.ts +++ b/tests/matching-flow.spec.ts @@ -3,7 +3,7 @@ import { expandResidentIntakeDetails } from './helpers' // storageState from playwright.config handles staff auth. // -// Seed data used (from prisma/seed.ts): +// Seed data used (from scripts/db/seed.ts): // - RES-008 to RES-013: status=ACTIVE (unplaced, no placement) // - ZH-001 to ZH-010+: housing units with available spots diff --git a/tests/portal.spec.ts b/tests/portal.spec.ts index a21c0135..3a9802b5 100644 --- a/tests/portal.spec.ts +++ b/tests/portal.spec.ts @@ -9,7 +9,7 @@ import { portalLocaleCookie } from './helpers' * - Portal pages require resident_code cookie → redirect to /login without it * - We inject the cookie directly via addCookies() — no login round-trip needed * - * Seed data used (from prisma/seed.ts): + * Seed data used (from scripts/db/seed.ts): * - RES-001: status=PLACED (has active placement) * - RES-021: status=ACTIVE (unplaced, no placement) */ diff --git a/tests/resident-detail.spec.ts b/tests/resident-detail.spec.ts index c1bd6a20..bae1e73f 100644 --- a/tests/resident-detail.spec.ts +++ b/tests/resident-detail.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test' import { expectNotFoundPage } from './helpers' // storageState from playwright.config handles staff auth -// Seed data (prisma/seed.ts): +// Seed data (scripts/db/seed.ts): // RES-001 → status PLACED (active placement in ZH-001) // RES-021 → status ACTIVE (unplaced, no active placement) From 0c31fb7835fb2525d895b00780f0895d16136cdc Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:30:49 +0200 Subject: [PATCH 4/5] refactor(db): port #152 (staff site access) to Drizzle - schema, filters, 0001 Master's StaffUnit/SiteAccess feature landed mid-migration in Prisma terms; this ports its data layer 1:1: siteAccess pgEnum + User column + StaffUnit table (pinned Prisma constraint names), unitAccess/staffAccess relations, and drizzle/0001_staff_site_access.sql whose DDL matches the Prisma migration statement for statement. Scratch parity re-proven with 0001 included: normalized pg_dump diff of the full Prisma chain (30 migrations) vs drizzle 0000+0001 is EMPTY. site-access.ts now emits drizzle where-fragments: inArray for unit/ housingUnitId scopes (callers name their table's column), the residents- with-an-ACTIVE-placement rule as an inArray subquery built on a standalone QueryBuilder (constructing a filter must not touch the lazy client - jest and next build call it without DATABASE_URL), and `sql\`false\`` for the assigned-nowhere case (drizzle's inArray throws on []). getCurrentUser carries siteAccess + assignedUnitIds off the row via `with: unitAccess`. The boards pass the fragments through `and(filter ?? undefined, ...)` so an ALL_UNITS viewer's query is unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn --- drizzle/0001_staff_site_access.sql | 13 + drizzle/meta/0000_snapshot.json | 1188 +-- drizzle/meta/0001_snapshot.json | 7581 +++++++++++++++++++ drizzle/meta/_journal.json | 9 +- src/lib/auth/__tests__/current-user.test.ts | 26 +- src/lib/auth/__tests__/site-access.test.ts | 71 +- src/lib/auth/index.ts | 4 +- src/lib/auth/site-access.ts | 70 +- src/lib/db/relations.ts | 1124 +-- src/lib/db/schema.ts | 43 + src/lib/db/types.ts | 606 +- 11 files changed, 8886 insertions(+), 1849 deletions(-) create mode 100644 drizzle/0001_staff_site_access.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0001_staff_site_access.sql b/drizzle/0001_staff_site_access.sql new file mode 100644 index 00000000..9443d741 --- /dev/null +++ b/drizzle/0001_staff_site_access.sql @@ -0,0 +1,13 @@ +CREATE TYPE "public"."SiteAccess" AS ENUM('ALL_UNITS', 'ASSIGNED_UNITS');--> statement-breakpoint +CREATE TABLE "StaffUnit" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + "staffId" text NOT NULL, + "housingUnitId" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "User" ADD COLUMN "siteAccess" "SiteAccess" DEFAULT 'ALL_UNITS' NOT NULL;--> statement-breakpoint +ALTER TABLE "StaffUnit" ADD CONSTRAINT "StaffUnit_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "StaffUnit" ADD CONSTRAINT "StaffUnit_housingUnitId_fkey" FOREIGN KEY ("housingUnitId") REFERENCES "public"."HousingUnit"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +CREATE UNIQUE INDEX "StaffUnit_staffId_housingUnitId_key" ON "StaffUnit" USING btree ("staffId","housingUnitId");--> statement-breakpoint +CREATE INDEX "StaffUnit_housingUnitId_idx" ON "StaffUnit" USING btree ("housingUnitId"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index f456bf11..e13d1d2e 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -140,12 +140,8 @@ "name": "Account_userId_fkey", "tableFrom": "Account", "tableTo": "User", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -153,12 +149,8 @@ "name": "Account_residentId_fkey", "tableFrom": "Account", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -353,12 +345,8 @@ "name": "Activity_createdByUserId_fkey", "tableFrom": "Activity", "tableTo": "User", - "columnsFrom": [ - "createdByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -366,12 +354,8 @@ "name": "Activity_updatedByUserId_fkey", "tableFrom": "Activity", "tableTo": "User", - "columnsFrom": [ - "updatedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -460,12 +444,8 @@ "name": "AgreementParty_agreementId_fkey", "tableFrom": "AgreementParty", "tableTo": "ConflictAgreement", - "columnsFrom": [ - "agreementId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["agreementId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -473,12 +453,8 @@ "name": "AgreementParty_residentId_fkey", "tableFrom": "AgreementParty", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -762,12 +738,8 @@ "name": "Appointment_residentId_fkey", "tableFrom": "Appointment", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -775,12 +747,8 @@ "name": "Appointment_staffId_fkey", "tableFrom": "Appointment", "tableTo": "User", - "columnsFrom": [ - "staffId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["staffId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -903,12 +871,8 @@ "name": "AuditLog_userId_fkey", "tableFrom": "AuditLog", "tableTo": "User", - "columnsFrom": [ - "userId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["userId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -1011,12 +975,8 @@ "name": "AuthToken_accountId_fkey", "tableFrom": "AuthToken", "tableTo": "Account", - "columnsFrom": [ - "accountId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["accountId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1113,12 +1073,8 @@ "name": "CareAssignment_residentId_fkey", "tableFrom": "CareAssignment", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1126,12 +1082,8 @@ "name": "CareAssignment_staffId_fkey", "tableFrom": "CareAssignment", "tableTo": "User", - "columnsFrom": [ - "staffId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["staffId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -1245,12 +1197,8 @@ "name": "CareAttribute_residentId_fkey", "tableFrom": "CareAttribute", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1258,12 +1206,8 @@ "name": "CareAttribute_updatedById_fkey", "tableFrom": "CareAttribute", "tableTo": "User", - "columnsFrom": [ - "updatedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["updatedById"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -1395,12 +1339,8 @@ "name": "CompatibilityAssessment_residentId_fkey", "tableFrom": "CompatibilityAssessment", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1408,12 +1348,8 @@ "name": "CompatibilityAssessment_comparedWithId_fkey", "tableFrom": "CompatibilityAssessment", "tableTo": "Resident", - "columnsFrom": [ - "comparedWithId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["comparedWithId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1545,12 +1481,8 @@ "name": "Complaint_residentId_fkey", "tableFrom": "Complaint", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -1558,12 +1490,8 @@ "name": "Complaint_respondedByUserId_fkey", "tableFrom": "Complaint", "tableTo": "User", - "columnsFrom": [ - "respondedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["respondedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -1706,12 +1634,8 @@ "name": "ConflictAgreement_incidentId_fkey", "tableFrom": "ConflictAgreement", "tableTo": "Incident", - "columnsFrom": [ - "incidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1719,12 +1643,8 @@ "name": "ConflictAgreement_ruleProposalId_fkey", "tableFrom": "ConflictAgreement", "tableTo": "Proposal", - "columnsFrom": [ - "ruleProposalId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ruleProposalId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -1816,12 +1736,8 @@ "name": "EventRsvp_eventId_fkey", "tableFrom": "EventRsvp", "tableTo": "HouseEvent", - "columnsFrom": [ - "eventId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["eventId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1829,12 +1745,8 @@ "name": "EventRsvp_residentId_fkey", "tableFrom": "EventRsvp", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1939,12 +1851,8 @@ "name": "Expense_housingUnitId_fkey", "tableFrom": "Expense", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1952,12 +1860,8 @@ "name": "Expense_paidById_fkey", "tableFrom": "Expense", "tableTo": "Resident", - "columnsFrom": [ - "paidById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["paidById"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -1965,12 +1869,8 @@ "name": "Expense_createdById_fkey", "tableFrom": "Expense", "tableTo": "Resident", - "columnsFrom": [ - "createdById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdById"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -2053,12 +1953,8 @@ "name": "ExpenseShare_expenseId_fkey", "tableFrom": "ExpenseShare", "tableTo": "Expense", - "columnsFrom": [ - "expenseId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["expenseId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2066,12 +1962,8 @@ "name": "ExpenseShare_residentId_fkey", "tableFrom": "ExpenseShare", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -2219,12 +2111,8 @@ "name": "HouseEvent_housingUnitId_fkey", "tableFrom": "HouseEvent", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2232,12 +2120,8 @@ "name": "HouseEvent_createdByStaffId_fkey", "tableFrom": "HouseEvent", "tableTo": "User", - "columnsFrom": [ - "createdByStaffId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdByStaffId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -2245,12 +2129,8 @@ "name": "HouseEvent_createdByResidentId_fkey", "tableFrom": "HouseEvent", "tableTo": "Resident", - "columnsFrom": [ - "createdByResidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -2471,12 +2351,8 @@ "name": "HouseRule_housingUnitId_fkey", "tableFrom": "HouseRule", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2484,12 +2360,8 @@ "name": "HouseRule_parentRuleId_fkey", "tableFrom": "HouseRule", "tableTo": "HouseRule", - "columnsFrom": [ - "parentRuleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parentRuleId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -2497,12 +2369,8 @@ "name": "HouseRule_adoptedByProposalId_fkey", "tableFrom": "HouseRule", "tableTo": "Proposal", - "columnsFrom": [ - "adoptedByProposalId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["adoptedByProposalId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -2693,12 +2561,8 @@ "name": "HouseholdTask_housingUnitId_fkey", "tableFrom": "HouseholdTask", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2706,12 +2570,8 @@ "name": "HouseholdTask_createdByResidentId_fkey", "tableFrom": "HouseholdTask", "tableTo": "Resident", - "columnsFrom": [ - "createdByResidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -3193,12 +3053,8 @@ "name": "Incident_housingUnitId_fkey", "tableFrom": "Incident", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -3206,12 +3062,8 @@ "name": "Incident_placementId_fkey", "tableFrom": "Incident", "tableTo": "Placement", - "columnsFrom": [ - "placementId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["placementId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -3219,12 +3071,8 @@ "name": "Incident_reportedById_fkey", "tableFrom": "Incident", "tableTo": "Resident", - "columnsFrom": [ - "reportedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -3232,12 +3080,8 @@ "name": "Incident_subjectId_fkey", "tableFrom": "Incident", "tableTo": "Resident", - "columnsFrom": [ - "subjectId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -3339,12 +3183,8 @@ "name": "IncidentFollowUp_incidentId_fkey", "tableFrom": "IncidentFollowUp", "tableTo": "Incident", - "columnsFrom": [ - "incidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -3429,12 +3269,8 @@ "name": "IncidentInvolvement_incidentId_fkey", "tableFrom": "IncidentInvolvement", "tableTo": "Incident", - "columnsFrom": [ - "incidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -3442,12 +3278,8 @@ "name": "IncidentInvolvement_residentId_fkey", "tableFrom": "IncidentInvolvement", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -3628,12 +3460,8 @@ "name": "LearningRecord_residentId_fkey", "tableFrom": "LearningRecord", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -3863,12 +3691,8 @@ "name": "MaintenanceRequest_housingUnitId_fkey", "tableFrom": "MaintenanceRequest", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -3876,12 +3700,8 @@ "name": "MaintenanceRequest_spotId_fkey", "tableFrom": "MaintenanceRequest", "tableTo": "PlacementSpot", - "columnsFrom": [ - "spotId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["spotId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -3889,12 +3709,8 @@ "name": "MaintenanceRequest_reportedById_fkey", "tableFrom": "MaintenanceRequest", "tableTo": "Resident", - "columnsFrom": [ - "reportedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -4055,12 +3871,8 @@ "name": "MarketplacePost_housingUnitId_fkey", "tableFrom": "MarketplacePost", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -4068,12 +3880,8 @@ "name": "MarketplacePost_postedById_fkey", "tableFrom": "MarketplacePost", "tableTo": "Resident", - "columnsFrom": [ - "postedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["postedById"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -4081,12 +3889,8 @@ "name": "MarketplacePost_claimedById_fkey", "tableFrom": "MarketplacePost", "tableTo": "Resident", - "columnsFrom": [ - "claimedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["claimedById"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -4173,12 +3977,8 @@ "name": "Message_threadId_fkey", "tableFrom": "Message", "tableTo": "MessageThread", - "columnsFrom": [ - "threadId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["threadId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -4186,12 +3986,8 @@ "name": "Message_authorResidentId_fkey", "tableFrom": "Message", "tableTo": "Resident", - "columnsFrom": [ - "authorResidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["authorResidentId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -4199,12 +3995,8 @@ "name": "Message_authorUserId_fkey", "tableFrom": "Message", "tableTo": "User", - "columnsFrom": [ - "authorUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["authorUserId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -4287,12 +4079,8 @@ "name": "MessageThread_residentId_fkey", "tableFrom": "MessageThread", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -4495,12 +4283,8 @@ "name": "Opportunity_createdByUserId_fkey", "tableFrom": "Opportunity", "tableTo": "User", - "columnsFrom": [ - "createdByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -4508,12 +4292,8 @@ "name": "Opportunity_updatedByUserId_fkey", "tableFrom": "Opportunity", "tableTo": "User", - "columnsFrom": [ - "updatedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -4694,12 +4474,8 @@ "name": "OpportunityApplication_residentId_fkey", "tableFrom": "OpportunityApplication", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -4707,12 +4483,8 @@ "name": "OpportunityApplication_opportunityId_fkey", "tableFrom": "OpportunityApplication", "tableTo": "Opportunity", - "columnsFrom": [ - "opportunityId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["opportunityId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -4720,12 +4492,8 @@ "name": "OpportunityApplication_supportedByUserId_fkey", "tableFrom": "OpportunityApplication", "tableTo": "User", - "columnsFrom": [ - "supportedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["supportedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -4733,12 +4501,8 @@ "name": "OpportunityApplication_learningRecordId_fkey", "tableFrom": "OpportunityApplication", "tableTo": "LearningRecord", - "columnsFrom": [ - "learningRecordId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["learningRecordId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -4984,12 +4748,8 @@ "name": "Placement_residentId_fkey", "tableFrom": "Placement", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -4997,12 +4757,8 @@ "name": "Placement_housingUnitId_fkey", "tableFrom": "Placement", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -5010,12 +4766,8 @@ "name": "Placement_spotId_fkey", "tableFrom": "Placement", "tableTo": "PlacementSpot", - "columnsFrom": [ - "spotId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["spotId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -5023,12 +4775,8 @@ "name": "Placement_relatedIncidentId_fkey", "tableFrom": "Placement", "tableTo": "Incident", - "columnsFrom": [ - "relatedIncidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["relatedIncidentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -5234,12 +4982,8 @@ "name": "PlacementSpot_housingUnitId_fkey", "tableFrom": "PlacementSpot", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -5247,12 +4991,8 @@ "name": "PlacementSpot_parentSpotId_fkey", "tableFrom": "PlacementSpot", "tableTo": "PlacementSpot", - "columnsFrom": [ - "parentSpotId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parentSpotId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -5488,12 +5228,8 @@ "name": "Proposal_housingUnitId_fkey", "tableFrom": "Proposal", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -5501,12 +5237,8 @@ "name": "Proposal_targetRuleId_fkey", "tableFrom": "Proposal", "tableTo": "HouseRule", - "columnsFrom": [ - "targetRuleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["targetRuleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -5514,12 +5246,8 @@ "name": "Proposal_parentOrgRuleId_fkey", "tableFrom": "Proposal", "tableTo": "HouseRule", - "columnsFrom": [ - "parentOrgRuleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parentOrgRuleId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -5527,12 +5255,8 @@ "name": "Proposal_proposedByResidentId_fkey", "tableFrom": "Proposal", "tableTo": "Resident", - "columnsFrom": [ - "proposedByResidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["proposedByResidentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -6017,12 +5741,8 @@ "name": "ResidentDocument_residentId_fkey", "tableFrom": "ResidentDocument", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6030,12 +5750,8 @@ "name": "ResidentDocument_uploadedByUserId_fkey", "tableFrom": "ResidentDocument", "tableTo": "User", - "columnsFrom": [ - "uploadedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["uploadedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -6069,12 +5785,8 @@ "name": "ResidentDocumentBlob_documentId_fkey", "tableFrom": "ResidentDocumentBlob", "tableTo": "ResidentDocument", - "columnsFrom": [ - "documentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["documentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -6120,12 +5832,8 @@ "name": "ResidentPhoto_residentId_fkey", "tableFrom": "ResidentPhoto", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -6221,12 +5929,8 @@ "name": "RuleAcknowledgement_ruleId_fkey", "tableFrom": "RuleAcknowledgement", "tableTo": "HouseRule", - "columnsFrom": [ - "ruleId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["ruleId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6234,12 +5938,8 @@ "name": "RuleAcknowledgement_residentId_fkey", "tableFrom": "RuleAcknowledgement", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -6421,12 +6121,8 @@ "name": "SatisfactionCheckIn_placementId_fkey", "tableFrom": "SatisfactionCheckIn", "tableTo": "Placement", - "columnsFrom": [ - "placementId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["placementId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6434,12 +6130,8 @@ "name": "SatisfactionCheckIn_appointmentId_fkey", "tableFrom": "SatisfactionCheckIn", "tableTo": "Appointment", - "columnsFrom": [ - "appointmentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["appointmentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -6447,12 +6139,8 @@ "name": "SatisfactionCheckIn_collectedByUserId_fkey", "tableFrom": "SatisfactionCheckIn", "tableTo": "User", - "columnsFrom": [ - "collectedByUserId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["collectedByUserId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -6533,12 +6221,8 @@ "name": "Settlement_housingUnitId_fkey", "tableFrom": "Settlement", "tableTo": "HousingUnit", - "columnsFrom": [ - "housingUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6546,12 +6230,8 @@ "name": "Settlement_fromId_fkey", "tableFrom": "Settlement", "tableTo": "Resident", - "columnsFrom": [ - "fromId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["fromId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -6559,12 +6239,8 @@ "name": "Settlement_toId_fkey", "tableFrom": "Settlement", "tableTo": "Resident", - "columnsFrom": [ - "toId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["toId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -6702,12 +6378,8 @@ "name": "TaskAttentionFlag_taskId_fkey", "tableFrom": "TaskAttentionFlag", "tableTo": "HouseholdTask", - "columnsFrom": [ - "taskId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["taskId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6715,12 +6387,8 @@ "name": "TaskAttentionFlag_flaggedById_fkey", "tableFrom": "TaskAttentionFlag", "tableTo": "Resident", - "columnsFrom": [ - "flaggedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["flaggedById"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6728,12 +6396,8 @@ "name": "TaskAttentionFlag_resolvedByCompletionId_fkey", "tableFrom": "TaskAttentionFlag", "tableTo": "TaskCompletion", - "columnsFrom": [ - "resolvedByCompletionId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["resolvedByCompletionId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -6837,12 +6501,8 @@ "name": "TaskCompletion_taskId_fkey", "tableFrom": "TaskCompletion", "tableTo": "HouseholdTask", - "columnsFrom": [ - "taskId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["taskId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6850,12 +6510,8 @@ "name": "TaskCompletion_completedById_fkey", "tableFrom": "TaskCompletion", "tableTo": "Resident", - "columnsFrom": [ - "completedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["completedById"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -6972,12 +6628,8 @@ "name": "TaskRequest_taskId_fkey", "tableFrom": "TaskRequest", "tableTo": "HouseholdTask", - "columnsFrom": [ - "taskId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["taskId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6985,12 +6637,8 @@ "name": "TaskRequest_requestedById_fkey", "tableFrom": "TaskRequest", "tableTo": "Resident", - "columnsFrom": [ - "requestedById" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requestedById"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -6998,12 +6646,8 @@ "name": "TaskRequest_requestedResidentId_fkey", "tableFrom": "TaskRequest", "tableTo": "Resident", - "columnsFrom": [ - "requestedResidentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["requestedResidentId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -7011,12 +6655,8 @@ "name": "TaskRequest_completionId_fkey", "tableFrom": "TaskRequest", "tableTo": "TaskCompletion", - "columnsFrom": [ - "completionId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["completionId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -7138,12 +6778,8 @@ "name": "TransferRequest_residentId_fkey", "tableFrom": "TransferRequest", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -7151,12 +6787,8 @@ "name": "TransferRequest_currentPlacementId_fkey", "tableFrom": "TransferRequest", "tableTo": "Placement", - "columnsFrom": [ - "currentPlacementId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["currentPlacementId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" }, @@ -7164,12 +6796,8 @@ "name": "TransferRequest_targetUnitId_fkey", "tableFrom": "TransferRequest", "tableTo": "HousingUnit", - "columnsFrom": [ - "targetUnitId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["targetUnitId"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "cascade" } @@ -7305,9 +6933,7 @@ "User_code_key": { "name": "User_code_key", "nullsNotDistinct": false, - "columns": [ - "code" - ] + "columns": ["code"] } }, "policies": {}, @@ -7400,12 +7026,8 @@ "name": "Vote_proposalId_fkey", "tableFrom": "Vote", "tableTo": "Proposal", - "columnsFrom": [ - "proposalId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["proposalId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -7413,12 +7035,8 @@ "name": "Vote_residentId_fkey", "tableFrom": "Vote", "tableTo": "Resident", - "columnsFrom": [ - "residentId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["residentId"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -7434,283 +7052,142 @@ "public.ActivityCategory": { "name": "ActivityCategory", "schema": "public", - "values": [ - "SPORT", - "LANGUAGE", - "CULTURE", - "COMMUNITY", - "FAMILY", - "SUPPORT" - ] + "values": ["SPORT", "LANGUAGE", "CULTURE", "COMMUNITY", "FAMILY", "SUPPORT"] }, "public.ActivityCost": { "name": "ActivityCost", "schema": "public", - "values": [ - "FREE", - "REDUCED", - "PAID" - ] + "values": ["FREE", "REDUCED", "PAID"] }, "public.ActivityStatus": { "name": "ActivityStatus", "schema": "public", - "values": [ - "DRAFT", - "PUBLISHED", - "ARCHIVED" - ] + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] }, "public.AgeRange": { "name": "AgeRange", "schema": "public", - "values": [ - "YOUNG_ADULT", - "ADULT", - "MIDDLE_AGED", - "SENIOR" - ] + "values": ["YOUNG_ADULT", "ADULT", "MIDDLE_AGED", "SENIOR"] }, "public.AgreementStatus": { "name": "AgreementStatus", "schema": "public", - "values": [ - "PROPOSED", - "ACCEPTED", - "HELD", - "BROKEN", - "EXPIRED" - ] + "values": ["PROPOSED", "ACCEPTED", "HELD", "BROKEN", "EXPIRED"] }, "public.ApplicationStage": { "name": "ApplicationStage", "schema": "public", - "values": [ - "INTERESTED", - "APPLIED", - "INTERVIEW", - "ACCEPTED", - "STARTED", - "ENDED", - "DECLINED" - ] + "values": ["INTERESTED", "APPLIED", "INTERVIEW", "ACCEPTED", "STARTED", "ENDED", "DECLINED"] }, "public.AppointmentStatus": { "name": "AppointmentStatus", "schema": "public", - "values": [ - "SCHEDULED", - "COMPLETED", - "CANCELLED", - "NO_SHOW", - "REQUESTED" - ] + "values": ["SCHEDULED", "COMPLETED", "CANCELLED", "NO_SHOW", "REQUESTED"] }, "public.AuthTokenPurpose": { "name": "AuthTokenPurpose", "schema": "public", - "values": [ - "VERIFY_EMAIL", - "RESET_PASSWORD" - ] + "values": ["VERIFY_EMAIL", "RESET_PASSWORD"] }, "public.CareRole": { "name": "CareRole", "schema": "public", - "values": [ - "HOUSING", - "SOCIAL", - "JOB", - "VOLUNTEERING" - ] + "values": ["HOUSING", "SOCIAL", "JOB", "VOLUNTEERING"] }, "public.CheckInType": { "name": "CheckInType", "schema": "public", - "values": [ - "INITIAL", - "REGULAR", - "AD_HOC", - "EXIT" - ] + "values": ["INITIAL", "REGULAR", "AD_HOC", "EXIT"] }, "public.ComplaintStatus": { "name": "ComplaintStatus", "schema": "public", - "values": [ - "OPEN", - "IN_REVIEW", - "ANSWERED" - ] + "values": ["OPEN", "IN_REVIEW", "ANSWERED"] }, "public.ComplaintSubject": { "name": "ComplaintSubject", "schema": "public", - "values": [ - "STAFF", - "ACCOMMODATION", - "DECISION", - "OTHER" - ] + "values": ["STAFF", "ACCOMMODATION", "DECISION", "OTHER"] }, "public.ConflictStyle": { "name": "ConflictStyle", "schema": "public", - "values": [ - "AVOIDANT", - "COOPERATIVE", - "DIRECT" - ] + "values": ["AVOIDANT", "COOPERATIVE", "DIRECT"] }, "public.DecisionMode": { "name": "DecisionMode", "schema": "public", - "values": [ - "RESIDENT_BINDING", - "RESIDENT_ADVISORY", - "STAFF_ONLY" - ] + "values": ["RESIDENT_BINDING", "RESIDENT_ADVISORY", "STAFF_ONLY"] }, "public.EndReason": { "name": "EndReason", "schema": "public", - "values": [ - "NATURAL", - "CONFLICT", - "REQUEST", - "CAPACITY", - "UPGRADE", - "OTHER" - ] + "values": ["NATURAL", "CONFLICT", "REQUEST", "CAPACITY", "UPGRADE", "OTHER"] }, "public.EventRsvpStatus": { "name": "EventRsvpStatus", "schema": "public", - "values": [ - "GOING", - "MAYBE", - "DECLINED" - ] + "values": ["GOING", "MAYBE", "DECLINED"] }, "public.FamilyStatus": { "name": "FamilyStatus", "schema": "public", - "values": [ - "SINGLE", - "COUPLE", - "FAMILY_WITH_CHILDREN", - "SINGLE_PARENT" - ] + "values": ["SINGLE", "COUPLE", "FAMILY_WITH_CHILDREN", "SINGLE_PARENT"] }, "public.FollowUpPriority": { "name": "FollowUpPriority", "schema": "public", - "values": [ - "LOW", - "NORMAL", - "HIGH", - "URGENT" - ] + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] }, "public.Gender": { "name": "Gender", "schema": "public", - "values": [ - "MALE", - "FEMALE", - "OTHER", - "PREFER_NOT_SAY" - ] + "values": ["MALE", "FEMALE", "OTHER", "PREFER_NOT_SAY"] }, "public.HouseEventCategory": { "name": "HouseEventCategory", "schema": "public", - "values": [ - "HOUSE_MEETING", - "SOCIAL", - "CULTURE", - "SUPPORT" - ] + "values": ["HOUSE_MEETING", "SOCIAL", "CULTURE", "SUPPORT"] }, "public.HouseEventStatus": { "name": "HouseEventStatus", "schema": "public", - "values": [ - "DRAFT", - "PUBLISHED", - "CANCELLED" - ] + "values": ["DRAFT", "PUBLISHED", "CANCELLED"] }, "public.HouseholdTaskCategory": { "name": "HouseholdTaskCategory", "schema": "public", - "values": [ - "CLEANING", - "SHOPPING", - "MAINTENANCE", - "COOKING", - "TRASH", - "OTHER" - ] + "values": ["CLEANING", "SHOPPING", "MAINTENANCE", "COOKING", "TRASH", "OTHER"] }, "public.HouseholdTaskPriority": { "name": "HouseholdTaskPriority", "schema": "public", - "values": [ - "LOW", - "NORMAL", - "HIGH", - "URGENT" - ] + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] }, "public.HouseholdTaskStatus": { "name": "HouseholdTaskStatus", "schema": "public", - "values": [ - "IDLE", - "NEEDS_ATTENTION", - "REQUESTED", - "IN_PROGRESS" - ] + "values": ["IDLE", "NEEDS_ATTENTION", "REQUESTED", "IN_PROGRESS"] }, "public.HouseholdTaskType": { "name": "HouseholdTaskType", "schema": "public", - "values": [ - "ONE_TIME", - "RECURRING_SCHEDULED", - "RECURRING_AS_NEEDED" - ] + "values": ["ONE_TIME", "RECURRING_SCHEDULED", "RECURRING_AS_NEEDED"] }, "public.HousingStatus": { "name": "HousingStatus", "schema": "public", - "values": [ - "AVAILABLE", - "FULL", - "MAINTENANCE", - "CLOSED" - ] + "values": ["AVAILABLE", "FULL", "MAINTENANCE", "CLOSED"] }, "public.IncidentCategory": { "name": "IncidentCategory", "schema": "public", - "values": [ - "INTERPERSONAL", - "MAINTENANCE", - "SAFETY", - "WELLBEING" - ] + "values": ["INTERPERSONAL", "MAINTENANCE", "SAFETY", "WELLBEING"] }, "public.IncidentSeverity": { "name": "IncidentSeverity", "schema": "public", - "values": [ - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ] + "values": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] }, "public.IncidentType": { "name": "IncidentType", @@ -7738,11 +7215,7 @@ "public.InvolvementRole": { "name": "InvolvementRole", "schema": "public", - "values": [ - "INVOLVED", - "WITNESS", - "MEDIATOR" - ] + "values": ["INVOLVED", "WITNESS", "MEDIATOR"] }, "public.LearningKind": { "name": "LearningKind", @@ -7761,21 +7234,12 @@ "public.LearningStatus": { "name": "LearningStatus", "schema": "public", - "values": [ - "PLANNED", - "IN_PROGRESS", - "COMPLETED", - "EXPIRED" - ] + "values": ["PLANNED", "IN_PROGRESS", "COMPLETED", "EXPIRED"] }, "public.LivingSkillsSupport": { "name": "LivingSkillsSupport", "schema": "public", - "values": [ - "INDEPENDENT", - "SOME_SUPPORT", - "REGULAR_SUPPORT" - ] + "values": ["INDEPENDENT", "SOME_SUPPORT", "REGULAR_SUPPORT"] }, "public.MaintenanceCategory": { "name": "MaintenanceCategory", @@ -7796,108 +7260,57 @@ "public.MaintenancePriority": { "name": "MaintenancePriority", "schema": "public", - "values": [ - "LOW", - "NORMAL", - "HIGH", - "URGENT" - ] + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] }, "public.MaintenanceStatus": { "name": "MaintenanceStatus", "schema": "public", - "values": [ - "OPEN", - "ASSIGNED", - "IN_PROGRESS", - "ON_HOLD", - "COMPLETED", - "CANCELLED" - ] + "values": ["OPEN", "ASSIGNED", "IN_PROGRESS", "ON_HOLD", "COMPLETED", "CANCELLED"] }, "public.MarketplacePostKind": { "name": "MarketplacePostKind", "schema": "public", - "values": [ - "GIVE_AWAY", - "LEND", - "WANTED", - "OFFER_HELP", - "NEED_HELP" - ] + "values": ["GIVE_AWAY", "LEND", "WANTED", "OFFER_HELP", "NEED_HELP"] }, "public.MarketplacePostStatus": { "name": "MarketplacePostStatus", "schema": "public", - "values": [ - "OPEN", - "CLAIMED", - "CLOSED" - ] + "values": ["OPEN", "CLAIMED", "CLOSED"] }, "public.MedicalDocType": { "name": "MedicalDocType", "schema": "public", - "values": [ - "PRIVATE_ROOM", - "STUDIO", - "BOTH" - ] + "values": ["PRIVATE_ROOM", "STUDIO", "BOTH"] }, "public.MobilityNeed": { "name": "MobilityNeed", "schema": "public", - "values": [ - "NONE", - "GROUND_FLOOR", - "WHEELCHAIR" - ] + "values": ["NONE", "GROUND_FLOOR", "WHEELCHAIR"] }, "public.OpportunityKind": { "name": "OpportunityKind", "schema": "public", - "values": [ - "VOLUNTEERING", - "COMMUNITY_SERVICE", - "EMPLOYMENT", - "INTERNSHIP" - ] + "values": ["VOLUNTEERING", "COMMUNITY_SERVICE", "EMPLOYMENT", "INTERNSHIP"] }, "public.OpportunityStatus": { "name": "OpportunityStatus", "schema": "public", - "values": [ - "DRAFT", - "PUBLISHED", - "ARCHIVED" - ] + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] }, "public.PermitRequirement": { "name": "PermitRequirement", "schema": "public", - "values": [ - "NONE", - "EMPLOYER_NOTIFIES", - "PERMIT_REQUIRED" - ] + "values": ["NONE", "EMPLOYER_NOTIFIES", "PERMIT_REQUIRED"] }, "public.PlacementStatus": { "name": "PlacementStatus", "schema": "public", - "values": [ - "ACTIVE", - "ENDED", - "TRANSFERRED" - ] + "values": ["ACTIVE", "ENDED", "TRANSFERRED"] }, "public.ProfileVisibility": { "name": "ProfileVisibility", "schema": "public", - "values": [ - "PRIVATE", - "ROOMMATES", - "RESIDENTS" - ] + "values": ["PRIVATE", "ROOMMATES", "RESIDENTS"] }, "public.ProposalStatus": { "name": "ProposalStatus", @@ -7916,39 +7329,22 @@ "public.ProposalType": { "name": "ProposalType", "schema": "public", - "values": [ - "ADD_RULE", - "AMEND_RULE", - "REPEAL_RULE", - "HOUSE_DECISION" - ] + "values": ["ADD_RULE", "AMEND_RULE", "REPEAL_RULE", "HOUSE_DECISION"] }, "public.RecyclingKnowledge": { "name": "RecyclingKnowledge", "schema": "public", - "values": [ - "NONE", - "BASIC", - "GOOD" - ] + "values": ["NONE", "BASIC", "GOOD"] }, "public.ResidentOrStaff": { "name": "ResidentOrStaff", "schema": "public", - "values": [ - "RESIDENT", - "STAFF" - ] + "values": ["RESIDENT", "STAFF"] }, "public.ResidentStatus": { "name": "ResidentStatus", "schema": "public", - "values": [ - "ACTIVE", - "PLACED", - "TRANSFERRED", - "EXITED" - ] + "values": ["ACTIVE", "PLACED", "TRANSFERRED", "EXITED"] }, "public.ResolutionStage": { "name": "ResolutionStage", @@ -7965,11 +7361,7 @@ "public.RoomSharingStatus": { "name": "RoomSharingStatus", "schema": "public", - "values": [ - "CAN_SHARE", - "PREFERS_PRIVATE", - "NEEDS_PRIVATE" - ] + "values": ["CAN_SHARE", "PREFERS_PRIVATE", "NEEDS_PRIVATE"] }, "public.RuleCategory": { "name": "RuleCategory", @@ -7991,152 +7383,82 @@ "public.RuleDelegation": { "name": "RuleDelegation", "schema": "public", - "values": [ - "FIXED", - "UNIT_MAY_STRENGTHEN", - "UNIT_DECIDES" - ] + "values": ["FIXED", "UNIT_MAY_STRENGTHEN", "UNIT_DECIDES"] }, "public.RuleScope": { "name": "RuleScope", "schema": "public", - "values": [ - "ORG", - "UNIT" - ] + "values": ["ORG", "UNIT"] }, "public.RuleStatus": { "name": "RuleStatus", "schema": "public", - "values": [ - "ACTIVE", - "SUPERSEDED", - "ARCHIVED" - ] + "values": ["ACTIVE", "SUPERSEDED", "ARCHIVED"] }, "public.SleepSchedule": { "name": "SleepSchedule", "schema": "public", - "values": [ - "EARLY_BIRD", - "STANDARD", - "NIGHT_OWL", - "IRREGULAR" - ] + "values": ["EARLY_BIRD", "STANDARD", "NIGHT_OWL", "IRREGULAR"] }, "public.SmokingStatus": { "name": "SmokingStatus", "schema": "public", - "values": [ - "NON_SMOKER", - "OUTDOOR_SMOKER", - "INDOOR_SMOKER" - ] + "values": ["NON_SMOKER", "OUTDOOR_SMOKER", "INDOOR_SMOKER"] }, "public.SocialStyle": { "name": "SocialStyle", "schema": "public", - "values": [ - "INTROVERTED", - "MODERATE", - "EXTROVERTED" - ] + "values": ["INTROVERTED", "MODERATE", "EXTROVERTED"] }, "public.SpotStatus": { "name": "SpotStatus", "schema": "public", - "values": [ - "AVAILABLE", - "OCCUPIED", - "MAINTENANCE", - "CLOSED" - ] + "values": ["AVAILABLE", "OCCUPIED", "MAINTENANCE", "CLOSED"] }, "public.SpotType": { "name": "SpotType", "schema": "public", - "values": [ - "BED", - "PRIVATE_ROOM", - "STUDIO", - "ROOM" - ] + "values": ["BED", "PRIVATE_ROOM", "STUDIO", "ROOM"] }, "public.StaffDecision": { "name": "StaffDecision", "schema": "public", - "values": [ - "CONFIRMED", - "VETOED" - ] + "values": ["CONFIRMED", "VETOED"] }, "public.StaffRole": { "name": "StaffRole", "schema": "public", - "values": [ - "ADMIN", - "BETREUUNG", - "SOZIALARBEIT", - "JOBCOACH", - "FREIWILLIGENARBEIT" - ] + "values": ["ADMIN", "BETREUUNG", "SOZIALARBEIT", "JOBCOACH", "FREIWILLIGENARBEIT"] }, "public.StaffScope": { "name": "StaffScope", "schema": "public", - "values": [ - "OWN_DOMAIN", - "ALL_DOMAINS" - ] + "values": ["OWN_DOMAIN", "ALL_DOMAINS"] }, "public.SupportLevel": { "name": "SupportLevel", "schema": "public", - "values": [ - "STANDARD", - "ELEVATED", - "INTENSIVE" - ] + "values": ["STANDARD", "ELEVATED", "INTENSIVE"] }, "public.TaskRequestStatus": { "name": "TaskRequestStatus", "schema": "public", - "values": [ - "PENDING", - "ACCEPTED", - "DECLINED", - "COMPLETED" - ] + "values": ["PENDING", "ACCEPTED", "DECLINED", "COMPLETED"] }, "public.TransferRequestStatus": { "name": "TransferRequestStatus", "schema": "public", - "values": [ - "PENDING", - "APPROVED", - "DENIED", - "COMPLETED", - "CANCELLED" - ] + "values": ["PENDING", "APPROVED", "DENIED", "COMPLETED", "CANCELLED"] }, "public.VoteChoice": { "name": "VoteChoice", "schema": "public", - "values": [ - "YES", - "NO", - "ABSTAIN", - "BLOCK" - ] + "values": ["YES", "NO", "ABSTAIN", "BLOCK"] }, "public.VoteThreshold": { "name": "VoteThreshold", "schema": "public", - "values": [ - "CONSENSUS", - "SUPERMAJORITY", - "SIMPLE_MAJORITY" - ] + "values": ["CONSENSUS", "SUPERMAJORITY", "SIMPLE_MAJORITY"] } }, "schemas": {}, @@ -8149,4 +7471,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..ecb95d8b --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,7581 @@ +{ + "id": "f57a3251-c84e-4bb5-b4b4-b4d90bf08291", + "prevId": "e44fa51c-d34d-4880-a554-e3551fc5c9f2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerifiedAt": { + "name": "emailVerifiedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_email_key": { + "name": "Account_email_key", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_idx": { + "name": "Account_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_key": { + "name": "Account_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_idx": { + "name": "Account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_key": { + "name": "Account_userId_key", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Account_userId_fkey": { + "name": "Account_userId_fkey", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Account_residentId_fkey": { + "name": "Account_residentId_fkey", + "tableFrom": "Account", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Activity": { + "name": "Activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "ActivityCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "ActivityCost", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FREE'" + }, + "costNote": { + "name": "costNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ActivityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "highlight": { + "name": "highlight", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Activity_endsAt_idx": { + "name": "Activity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_category_idx": { + "name": "Activity_status_category_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_highlight_idx": { + "name": "Activity_status_highlight_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "highlight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Activity_createdByUserId_fkey": { + "name": "Activity_createdByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Activity_updatedByUserId_fkey": { + "name": "Activity_updatedByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AgreementParty": { + "name": "AgreementParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agreementId": { + "name": "agreementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "declinedAt": { + "name": "declinedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AgreementParty_agreementId_residentId_key": { + "name": "AgreementParty_agreementId_residentId_key", + "columns": [ + { + "expression": "agreementId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AgreementParty_residentId_idx": { + "name": "AgreementParty_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AgreementParty_agreementId_fkey": { + "name": "AgreementParty_agreementId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "ConflictAgreement", + "columnsFrom": ["agreementId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "AgreementParty_residentId_fkey": { + "name": "AgreementParty_residentId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AlgorithmWeight": { + "name": "AlgorithmWeight", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "lifestyleWeight": { + "name": "lifestyleWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "socialWeight": { + "name": "socialWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "practicalWeight": { + "name": "practicalWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "riskWeight": { + "name": "riskWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "factorWeights": { + "name": "factorWeights", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AlgorithmWeight_active_idx": { + "name": "AlgorithmWeight_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Appointment": { + "name": "Appointment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "AppointmentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SCHEDULED'" + }, + "residentNote": { + "name": "residentNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffNote": { + "name": "staffNote", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Appointment_residentId_startsAt_idx": { + "name": "Appointment_residentId_startsAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_staffId_startsAt_idx": { + "name": "Appointment_staffId_startsAt_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_domain_idx": { + "name": "Appointment_status_domain_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_idx": { + "name": "Appointment_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Appointment_residentId_fkey": { + "name": "Appointment_residentId_fkey", + "tableFrom": "Appointment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Appointment_staffId_fkey": { + "name": "Appointment_staffId_fkey", + "tableFrom": "Appointment", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuditLog": { + "name": "AuditLog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entityId": { + "name": "entityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AuditLog_createdAt_idx": { + "name": "AuditLog_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_entity_entityId_idx": { + "name": "AuditLog_entity_entityId_idx", + "columns": [ + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_userId_idx": { + "name": "AuditLog_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuditLog_userId_fkey": { + "name": "AuditLog_userId_fkey", + "tableFrom": "AuditLog", + "tableTo": "User", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuthToken": { + "name": "AuthToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "AuthTokenPurpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "usedAt": { + "name": "usedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "AuthToken_accountId_purpose_idx": { + "name": "AuthToken_accountId_purpose_idx", + "columns": [ + { + "expression": "accountId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purpose", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuthToken_tokenHash_key": { + "name": "AuthToken_tokenHash_key", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuthToken_accountId_fkey": { + "name": "AuthToken_accountId_fkey", + "tableFrom": "AuthToken", + "tableTo": "Account", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAssignment": { + "name": "CareAssignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAssignment_residentId_role_key": { + "name": "CareAssignment_residentId_role_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAssignment_staffId_idx": { + "name": "CareAssignment_staffId_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAssignment_residentId_fkey": { + "name": "CareAssignment_residentId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAssignment_staffId_fkey": { + "name": "CareAssignment_staffId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAttribute": { + "name": "CareAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedById": { + "name": "updatedById", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAttribute_residentId_domain_idx": { + "name": "CareAttribute_residentId_domain_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAttribute_residentId_domain_key_key": { + "name": "CareAttribute_residentId_domain_key_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAttribute_residentId_fkey": { + "name": "CareAttribute_residentId_fkey", + "tableFrom": "CareAttribute", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAttribute_updatedById_fkey": { + "name": "CareAttribute_updatedById_fkey", + "tableFrom": "CareAttribute", + "tableTo": "User", + "columnsFrom": ["updatedById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CompatibilityAssessment": { + "name": "CompatibilityAssessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparedWithId": { + "name": "comparedWithId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overallScore": { + "name": "overallScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "strengths": { + "name": "strengths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "recommendations": { + "name": "recommendations", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "CompatibilityAssessment_overallScore_idx": { + "name": "CompatibilityAssessment_overallScore_idx", + "columns": [ + { + "expression": "overallScore", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CompatibilityAssessment_residentId_comparedWithId_key": { + "name": "CompatibilityAssessment_residentId_comparedWithId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "comparedWithId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CompatibilityAssessment_residentId_fkey": { + "name": "CompatibilityAssessment_residentId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CompatibilityAssessment_comparedWithId_fkey": { + "name": "CompatibilityAssessment_comparedWithId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": ["comparedWithId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Complaint": { + "name": "Complaint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "ComplaintSubject", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ComplaintStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "respondedAt": { + "name": "respondedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "respondedByUserId": { + "name": "respondedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Complaint_createdAt_idx": { + "name": "Complaint_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_residentId_idx": { + "name": "Complaint_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_status_idx": { + "name": "Complaint_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Complaint_residentId_fkey": { + "name": "Complaint_residentId_fkey", + "tableFrom": "Complaint", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Complaint_respondedByUserId_fkey": { + "name": "Complaint_respondedByUserId_fkey", + "tableFrom": "Complaint", + "tableTo": "User", + "columnsFrom": ["respondedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ConflictAgreement": { + "name": "ConflictAgreement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms": { + "name": "terms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mediatorName": { + "name": "mediatorName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewDate": { + "name": "reviewDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "AgreementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PROPOSED'" + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "ruleProposalId": { + "name": "ruleProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ConflictAgreement_incidentId_idx": { + "name": "ConflictAgreement_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_ruleProposalId_key": { + "name": "ConflictAgreement_ruleProposalId_key", + "columns": [ + { + "expression": "ruleProposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_status_reviewDate_idx": { + "name": "ConflictAgreement_status_reviewDate_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reviewDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ConflictAgreement_incidentId_fkey": { + "name": "ConflictAgreement_incidentId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ConflictAgreement_ruleProposalId_fkey": { + "name": "ConflictAgreement_ruleProposalId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Proposal", + "columnsFrom": ["ruleProposalId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EventRsvp": { + "name": "EventRsvp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "EventRsvpStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GOING'" + } + }, + "indexes": { + "EventRsvp_eventId_idx": { + "name": "EventRsvp_eventId_idx", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EventRsvp_eventId_residentId_key": { + "name": "EventRsvp_eventId_residentId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EventRsvp_eventId_fkey": { + "name": "EventRsvp_eventId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "HouseEvent", + "columnsFrom": ["eventId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EventRsvp_residentId_fkey": { + "name": "EventRsvp_residentId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Expense": { + "name": "Expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paidById": { + "name": "paidById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdById": { + "name": "createdById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Expense_housingUnitId_date_idx": { + "name": "Expense_housingUnitId_date_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Expense_housingUnitId_fkey": { + "name": "Expense_housingUnitId_fkey", + "tableFrom": "Expense", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Expense_paidById_fkey": { + "name": "Expense_paidById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": ["paidById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Expense_createdById_fkey": { + "name": "Expense_createdById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": ["createdById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ExpenseShare": { + "name": "ExpenseShare", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expenseId": { + "name": "expenseId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ExpenseShare_expenseId_residentId_key": { + "name": "ExpenseShare_expenseId_residentId_key", + "columns": [ + { + "expression": "expenseId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ExpenseShare_residentId_idx": { + "name": "ExpenseShare_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ExpenseShare_expenseId_fkey": { + "name": "ExpenseShare_expenseId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Expense", + "columnsFrom": ["expenseId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ExpenseShare_residentId_fkey": { + "name": "ExpenseShare_residentId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseEvent": { + "name": "HouseEvent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "HouseEventCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SOCIAL'" + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "HouseEventStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLISHED'" + }, + "createdByStaffId": { + "name": "createdByStaffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseEvent_housingUnitId_startsAt_idx": { + "name": "HouseEvent_housingUnitId_startsAt_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseEvent_status_startsAt_idx": { + "name": "HouseEvent_status_startsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseEvent_housingUnitId_fkey": { + "name": "HouseEvent_housingUnitId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseEvent_createdByStaffId_fkey": { + "name": "HouseEvent_createdByStaffId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "User", + "columnsFrom": ["createdByStaffId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "HouseEvent_createdByResidentId_fkey": { + "name": "HouseEvent_createdByResidentId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "Resident", + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseRule": { + "name": "HouseRule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "RuleScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegation": { + "name": "delegation", + "type": "RuleDelegation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FIXED'" + }, + "parentRuleId": { + "name": "parentRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "RuleStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "effectiveFrom": { + "name": "effectiveFrom", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "effectiveUntil": { + "name": "effectiveUntil", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "adoptedByProposalId": { + "name": "adoptedByProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseRule_category_idx": { + "name": "HouseRule_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_housingUnitId_status_idx": { + "name": "HouseRule_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_key_key": { + "name": "HouseRule_key_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_parentRuleId_idx": { + "name": "HouseRule_parentRuleId_idx", + "columns": [ + { + "expression": "parentRuleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_scope_status_idx": { + "name": "HouseRule_scope_status_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseRule_housingUnitId_fkey": { + "name": "HouseRule_housingUnitId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseRule_parentRuleId_fkey": { + "name": "HouseRule_parentRuleId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HouseRule", + "columnsFrom": ["parentRuleId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "HouseRule_adoptedByProposalId_fkey": { + "name": "HouseRule_adoptedByProposalId_fkey", + "tableFrom": "HouseRule", + "tableTo": "Proposal", + "columnsFrom": ["adoptedByProposalId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseholdTask": { + "name": "HouseholdTask", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taskType": { + "name": "taskType", + "type": "HouseholdTaskType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ONE_TIME'" + }, + "category": { + "name": "category", + "type": "HouseholdTaskCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "priority": { + "name": "priority", + "type": "HouseholdTaskPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "scheduleHuman": { + "name": "scheduleHuman", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimatedMinutes": { + "name": "estimatedMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currentStatus": { + "name": "currentStatus", + "type": "HouseholdTaskStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'IDLE'" + }, + "isCompleted": { + "name": "isCompleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checklist": { + "name": "checklist", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + }, + "rotationResidentIds": { + "name": "rotationResidentIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "HouseholdTask_housingUnitId_category_idx": { + "name": "HouseholdTask_housingUnitId_category_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseholdTask_housingUnitId_currentStatus_idx": { + "name": "HouseholdTask_housingUnitId_currentStatus_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currentStatus", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseholdTask_housingUnitId_fkey": { + "name": "HouseholdTask_housingUnitId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseholdTask_createdByResidentId_fkey": { + "name": "HouseholdTask_createdByResidentId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "Resident", + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HousingUnit": { + "name": "HousingUnit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totalBeds": { + "name": "totalBeds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "totalRooms": { + "name": "totalRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedRooms": { + "name": "sharedRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateRooms": { + "name": "privateRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedBathrooms": { + "name": "sharedBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateBathrooms": { + "name": "privateBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privateKitchen": { + "name": "privateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "groundFloor": { + "name": "groundFloor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "wheelchairAccess": { + "name": "wheelchairAccess", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elevator": { + "name": "elevator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "smokingAllowed": { + "name": "smokingAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petsAllowed": { + "name": "petsAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "quietHours": { + "name": "quietHours", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nearPublicTransport": { + "name": "nearPublicTransport", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "nearHealthServices": { + "name": "nearHealthServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nearSchools": { + "name": "nearSchools", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "HousingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildingCode": { + "name": "buildingCode", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HousingUnit_buildingCode_idx": { + "name": "HousingUnit_buildingCode_idx", + "columns": [ + { + "expression": "buildingCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_code_key": { + "name": "HousingUnit_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_status_idx": { + "name": "HousingUnit_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_totalBeds_idx": { + "name": "HousingUnit_totalBeds_idx", + "columns": [ + { + "expression": "totalBeds", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Incident": { + "name": "Incident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subjectId": { + "name": "subjectId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "IncidentCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERPERSONAL'" + }, + "type": { + "name": "type", + "type": "IncidentType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "IncidentSeverity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "predictable": { + "name": "predictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compatibilityGap": { + "name": "compatibilityGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nextFollowUpDate": { + "name": "nextFollowUpDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "followUpPriority": { + "name": "followUpPriority", + "type": "FollowUpPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "mediationMinutes": { + "name": "mediationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolutionStage": { + "name": "resolutionStage", + "type": "ResolutionStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'REPORTED'" + }, + "stageEnteredAt": { + "name": "stageEnteredAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Incident_date_idx": { + "name": "Incident_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_nextFollowUpDate_idx": { + "name": "Incident_nextFollowUpDate_idx", + "columns": [ + { + "expression": "nextFollowUpDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_reportedById_idx": { + "name": "Incident_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_subjectId_idx": { + "name": "Incident_subjectId_idx", + "columns": [ + { + "expression": "subjectId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_type_severity_idx": { + "name": "Incident_type_severity_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Incident_housingUnitId_fkey": { + "name": "Incident_housingUnitId_fkey", + "tableFrom": "Incident", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Incident_placementId_fkey": { + "name": "Incident_placementId_fkey", + "tableFrom": "Incident", + "tableTo": "Placement", + "columnsFrom": ["placementId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_reportedById_fkey": { + "name": "Incident_reportedById_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_subjectId_fkey": { + "name": "Incident_subjectId_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentFollowUp": { + "name": "IncidentFollowUp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffName": { + "name": "staffName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduledNextDate": { + "name": "scheduledNextDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IncidentFollowUp_createdAt_idx": { + "name": "IncidentFollowUp_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentFollowUp_incidentId_idx": { + "name": "IncidentFollowUp_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentFollowUp_incidentId_fkey": { + "name": "IncidentFollowUp_incidentId_fkey", + "tableFrom": "IncidentFollowUp", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentInvolvement": { + "name": "IncidentInvolvement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "InvolvementRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INVOLVED'" + } + }, + "indexes": { + "IncidentInvolvement_incidentId_residentId_key": { + "name": "IncidentInvolvement_incidentId_residentId_key", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentInvolvement_residentId_idx": { + "name": "IncidentInvolvement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentInvolvement_incidentId_fkey": { + "name": "IncidentInvolvement_incidentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "IncidentInvolvement_residentId_fkey": { + "name": "IncidentInvolvement_residentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.LearningRecord": { + "name": "LearningRecord", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "LearningKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "LearningStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "languageCode": { + "name": "languageCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cefrLevel": { + "name": "cefrLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recordedBy": { + "name": "recordedBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LearningRecord_languageCode_cefrLevel_idx": { + "name": "LearningRecord_languageCode_cefrLevel_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cefrLevel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_residentId_kind_idx": { + "name": "LearningRecord_residentId_kind_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_status_idx": { + "name": "LearningRecord_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "LearningRecord_residentId_fkey": { + "name": "LearningRecord_residentId_fkey", + "tableFrom": "LearningRecord", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MaintenanceRequest": { + "name": "MaintenanceRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "MaintenanceCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "MaintenancePriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reporterName": { + "name": "reporterName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedTo": { + "name": "assignedTo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "MaintenanceStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MaintenanceRequest_createdAt_idx": { + "name": "MaintenanceRequest_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_housingUnitId_idx": { + "name": "MaintenanceRequest_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_priority_status_idx": { + "name": "MaintenanceRequest_priority_status_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_reportedById_idx": { + "name": "MaintenanceRequest_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_status_idx": { + "name": "MaintenanceRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MaintenanceRequest_housingUnitId_fkey": { + "name": "MaintenanceRequest_housingUnitId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MaintenanceRequest_spotId_fkey": { + "name": "MaintenanceRequest_spotId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "PlacementSpot", + "columnsFrom": ["spotId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "MaintenanceRequest_reportedById_fkey": { + "name": "MaintenanceRequest_reportedById_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "Resident", + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MarketplacePost": { + "name": "MarketplacePost", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postedById": { + "name": "postedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "MarketplacePostKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "status": { + "name": "status", + "type": "MarketplacePostStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "claimedById": { + "name": "claimedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "closedAt": { + "name": "closedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "hiddenByStaff": { + "name": "hiddenByStaff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hiddenReason": { + "name": "hiddenReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactNote": { + "name": "contactNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimedAt": { + "name": "claimedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MarketplacePost_housingUnitId_status_idx": { + "name": "MarketplacePost_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MarketplacePost_postedById_idx": { + "name": "MarketplacePost_postedById_idx", + "columns": [ + { + "expression": "postedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MarketplacePost_housingUnitId_fkey": { + "name": "MarketplacePost_housingUnitId_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_postedById_fkey": { + "name": "MarketplacePost_postedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": ["postedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_claimedById_fkey": { + "name": "MarketplacePost_claimedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": ["claimedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Message": { + "name": "Message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "threadId": { + "name": "threadId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorResidentId": { + "name": "authorResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorUserId": { + "name": "authorUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "readAt": { + "name": "readAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Message_threadId_createdAt_idx": { + "name": "Message_threadId_createdAt_idx", + "columns": [ + { + "expression": "threadId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Message_threadId_fkey": { + "name": "Message_threadId_fkey", + "tableFrom": "Message", + "tableTo": "MessageThread", + "columnsFrom": ["threadId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Message_authorResidentId_fkey": { + "name": "Message_authorResidentId_fkey", + "tableFrom": "Message", + "tableTo": "Resident", + "columnsFrom": ["authorResidentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Message_authorUserId_fkey": { + "name": "Message_authorUserId_fkey", + "tableFrom": "Message", + "tableTo": "User", + "columnsFrom": ["authorUserId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "Message_one_author": { + "name": "Message_one_author", + "value": "(\"authorResidentId\" IS NOT NULL) <> (\"authorUserId\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.MessageThread": { + "name": "MessageThread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "MessageThread_residentId_key": { + "name": "MessageThread_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MessageThread_updatedAt_idx": { + "name": "MessageThread_updatedAt_idx", + "columns": [ + { + "expression": "updatedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MessageThread_residentId_fkey": { + "name": "MessageThread_residentId_fkey", + "tableFrom": "MessageThread", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Opportunity": { + "name": "Opportunity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "OpportunityKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organisation": { + "name": "organisation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hoursPerWeek": { + "name": "hoursPerWeek", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "germanLevel": { + "name": "germanLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permitRequirement": { + "name": "permitRequirement", + "type": "PermitRequirement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "requirementNote": { + "name": "requirementNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactName": { + "name": "contactName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactEmail": { + "name": "contactEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactPhone": { + "name": "contactPhone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "OpportunityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Opportunity_endsAt_idx": { + "name": "Opportunity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Opportunity_status_kind_idx": { + "name": "Opportunity_status_kind_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Opportunity_createdByUserId_fkey": { + "name": "Opportunity_createdByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Opportunity_updatedByUserId_fkey": { + "name": "Opportunity_updatedByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpportunityApplication": { + "name": "OpportunityApplication", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunityId": { + "name": "opportunityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "ApplicationStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERESTED'" + }, + "stageChangedAt": { + "name": "stageChangedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supportedByUserId": { + "name": "supportedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "learningRecordId": { + "name": "learningRecordId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "OpportunityApplication_learningRecordId_key": { + "name": "OpportunityApplication_learningRecordId_key", + "columns": [ + { + "expression": "learningRecordId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_opportunityId_stage_idx": { + "name": "OpportunityApplication_opportunityId_stage_idx", + "columns": [ + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_idx": { + "name": "OpportunityApplication_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_opportunityId_key": { + "name": "OpportunityApplication_residentId_opportunityId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_stage_idx": { + "name": "OpportunityApplication_stage_idx", + "columns": [ + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "OpportunityApplication_residentId_fkey": { + "name": "OpportunityApplication_residentId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "OpportunityApplication_opportunityId_fkey": { + "name": "OpportunityApplication_opportunityId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Opportunity", + "columnsFrom": ["opportunityId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "OpportunityApplication_supportedByUserId_fkey": { + "name": "OpportunityApplication_supportedByUserId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "User", + "columnsFrom": ["supportedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "OpportunityApplication_learningRecordId_fkey": { + "name": "OpportunityApplication_learningRecordId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "LearningRecord", + "columnsFrom": ["learningRecordId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Placement": { + "name": "Placement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "compatibilityScore": { + "name": "compatibilityScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "PlacementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "endReason": { + "name": "endReason", + "type": "EndReason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "satisfactionRating": { + "name": "satisfactionRating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "placementNotes": { + "name": "placementNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictGap": { + "name": "conflictGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wasPredictable": { + "name": "wasPredictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "relatedIncidentId": { + "name": "relatedIncidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Placement_housingUnitId_idx": { + "name": "Placement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_housingUnitId_startDate_key": { + "name": "Placement_residentId_housingUnitId_startDate_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_idx": { + "name": "Placement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_startDate_endDate_idx": { + "name": "Placement_startDate_endDate_idx", + "columns": [ + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_status_idx": { + "name": "Placement_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Placement_residentId_fkey": { + "name": "Placement_residentId_fkey", + "tableFrom": "Placement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_housingUnitId_fkey": { + "name": "Placement_housingUnitId_fkey", + "tableFrom": "Placement", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_spotId_fkey": { + "name": "Placement_spotId_fkey", + "tableFrom": "Placement", + "tableTo": "PlacementSpot", + "columnsFrom": ["spotId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Placement_relatedIncidentId_fkey": { + "name": "Placement_relatedIncidentId_fkey", + "tableFrom": "Placement", + "tableTo": "Incident", + "columnsFrom": ["relatedIncidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PlacementSpot": { + "name": "PlacementSpot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "SpotType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parentSpotId": { + "name": "parentSpotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "squareMeters": { + "name": "squareMeters", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hasPrivateBathroom": { + "name": "hasPrivateBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateKitchen": { + "name": "hasPrivateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateToilet": { + "name": "hasPrivateToilet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requiresMedicalDocs": { + "name": "requiresMedicalDocs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "SpotStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "PlacementSpot_housingUnitId_code_key": { + "name": "PlacementSpot_housingUnitId_code_key", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_housingUnitId_idx": { + "name": "PlacementSpot_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_requiresMedicalDocs_idx": { + "name": "PlacementSpot_requiresMedicalDocs_idx", + "columns": [ + { + "expression": "requiresMedicalDocs", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_type_status_idx": { + "name": "PlacementSpot_type_status_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "PlacementSpot_housingUnitId_fkey": { + "name": "PlacementSpot_housingUnitId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "PlacementSpot_parentSpotId_fkey": { + "name": "PlacementSpot_parentSpotId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "PlacementSpot", + "columnsFrom": ["parentSpotId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Proposal": { + "name": "Proposal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ProposalType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "targetRuleId": { + "name": "targetRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parentOrgRuleId": { + "name": "parentOrgRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByResidentId": { + "name": "proposedByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByStaff": { + "name": "proposedByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ProposalStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DISCUSSION'" + }, + "decisionMode": { + "name": "decisionMode", + "type": "DecisionMode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "VoteThreshold", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quorumPercent": { + "name": "quorumPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "approvalPercent": { + "name": "approvalPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "eligibleVoterCount": { + "name": "eligibleVoterCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discussionEndsAt": { + "name": "discussionEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingOpenedAt": { + "name": "votingOpenedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingEndsAt": { + "name": "votingEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "decidedAt": { + "name": "decidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "outcomeSummary": { + "name": "outcomeSummary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecision": { + "name": "staffDecision", + "type": "StaffDecision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffUserId": { + "name": "staffUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecidedAt": { + "name": "staffDecidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Proposal_housingUnitId_status_idx": { + "name": "Proposal_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Proposal_status_votingEndsAt_idx": { + "name": "Proposal_status_votingEndsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "votingEndsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Proposal_housingUnitId_fkey": { + "name": "Proposal_housingUnitId_fkey", + "tableFrom": "Proposal", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_targetRuleId_fkey": { + "name": "Proposal_targetRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": ["targetRuleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_parentOrgRuleId_fkey": { + "name": "Proposal_parentOrgRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": ["parentOrgRuleId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Proposal_proposedByResidentId_fkey": { + "name": "Proposal_proposedByResidentId_fkey", + "tableFrom": "Proposal", + "tableTo": "Resident", + "columnsFrom": ["proposedByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Resident": { + "name": "Resident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ageRange": { + "name": "ageRange", + "type": "AgeRange", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "Gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "familyStatus": { + "name": "familyStatus", + "type": "FamilyStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sleepSchedule": { + "name": "sleepSchedule", + "type": "SleepSchedule", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "noiseTolerance": { + "name": "noiseTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cleanlinessPractice": { + "name": "cleanlinessPractice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "guestTolerance": { + "name": "guestTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "socialStyle": { + "name": "socialStyle", + "type": "SocialStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "culturalRegion": { + "name": "culturalRegion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictStyle": { + "name": "conflictStyle", + "type": "ConflictStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'COOPERATIVE'" + }, + "smokingStatus": { + "name": "smokingStatus", + "type": "SmokingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dietaryNeeds": { + "name": "dietaryNeeds", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "mobilityNeeds": { + "name": "mobilityNeeds", + "type": "MobilityNeed", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "medicalEquipment": { + "name": "medicalEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petTolerance": { + "name": "petTolerance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedBathroom": { + "name": "sharedBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privacyNeed": { + "name": "privacyNeed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "choresContribution": { + "name": "choresContribution", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "recyclingKnowledge": { + "name": "recyclingKnowledge", + "type": "RecyclingKnowledge", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "roomSharingStatus": { + "name": "roomSharingStatus", + "type": "RoomSharingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CAN_SHARE'" + }, + "hasNightDisturbances": { + "name": "hasNightDisturbances", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needsQuietEnvironment": { + "name": "needsQuietEnvironment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasSleepEquipment": { + "name": "hasSleepEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "supportLevel": { + "name": "supportLevel", + "type": "SupportLevel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'STANDARD'" + }, + "roommatePreferences": { + "name": "roommatePreferences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ResidentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasMedicalDocumentation": { + "name": "hasMedicalDocumentation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "medicalDocType": { + "name": "medicalDocType", + "type": "MedicalDocType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "medicalDocDate": { + "name": "medicalDocDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "medicalDocNotes": { + "name": "medicalDocNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferencesCompletedAt": { + "name": "preferencesCompletedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "cleanlinessExpectation": { + "name": "cleanlinessExpectation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "chaosTolerance": { + "name": "chaosTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profileVisibility": { + "name": "profileVisibility", + "type": "ProfileVisibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ROOMMATES'" + }, + "livingSkillsSupport": { + "name": "livingSkillsSupport", + "type": "LivingSkillsSupport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INDEPENDENT'" + } + }, + "indexes": { + "Resident_ageRange_gender_idx": { + "name": "Resident_ageRange_gender_idx", + "columns": [ + { + "expression": "ageRange", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gender", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_code_key": { + "name": "Resident_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_livingSkillsSupport_idx": { + "name": "Resident_livingSkillsSupport_idx", + "columns": [ + { + "expression": "livingSkillsSupport", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_status_idx": { + "name": "Resident_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocument": { + "name": "ResidentDocument", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizeBytes": { + "name": "sizeBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadedByUserId": { + "name": "uploadedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ResidentDocument_residentId_createdAt_idx": { + "name": "ResidentDocument_residentId_createdAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ResidentDocument_residentId_fkey": { + "name": "ResidentDocument_residentId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ResidentDocument_uploadedByUserId_fkey": { + "name": "ResidentDocument_uploadedByUserId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "User", + "columnsFrom": ["uploadedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocumentBlob": { + "name": "ResidentDocumentBlob", + "schema": "", + "columns": { + "documentId": { + "name": "documentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentDocumentBlob_documentId_fkey": { + "name": "ResidentDocumentBlob_documentId_fkey", + "tableFrom": "ResidentDocumentBlob", + "tableTo": "ResidentDocument", + "columnsFrom": ["documentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentPhoto": { + "name": "ResidentPhoto", + "schema": "", + "columns": { + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentPhoto_residentId_fkey": { + "name": "ResidentPhoto_residentId_fkey", + "tableFrom": "ResidentPhoto", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RuleAcknowledgement": { + "name": "RuleAcknowledgement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ruleVersion": { + "name": "ruleVersion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "acknowledgedAt": { + "name": "acknowledgedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "RuleAcknowledgement_residentId_idx": { + "name": "RuleAcknowledgement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "RuleAcknowledgement_ruleId_residentId_ruleVersion_key": { + "name": "RuleAcknowledgement_ruleId_residentId_ruleVersion_key", + "columns": [ + { + "expression": "ruleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ruleVersion", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RuleAcknowledgement_ruleId_fkey": { + "name": "RuleAcknowledgement_ruleId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "HouseRule", + "columnsFrom": ["ruleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RuleAcknowledgement_residentId_fkey": { + "name": "RuleAcknowledgement_residentId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SatisfactionCheckIn": { + "name": "SatisfactionCheckIn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkInType": { + "name": "checkInType", + "type": "CheckInType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "weekNumber": { + "name": "weekNumber", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "overallSatisfaction": { + "name": "overallSatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roommateRelations": { + "name": "roommateRelations", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "facilitySatisfaction": { + "name": "facilitySatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "safetyFeeling": { + "name": "safetyFeeling", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "improvements": { + "name": "improvements", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "positives": { + "name": "positives", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedBy": { + "name": "collectedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isAnonymous": { + "name": "isAnonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appointmentId": { + "name": "appointmentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedByUserId": { + "name": "collectedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "SatisfactionCheckIn_appointmentId_key": { + "name": "SatisfactionCheckIn_appointmentId_key", + "columns": [ + { + "expression": "appointmentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_checkInType_idx": { + "name": "SatisfactionCheckIn_checkInType_idx", + "columns": [ + { + "expression": "checkInType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_collectedByUserId_idx": { + "name": "SatisfactionCheckIn_collectedByUserId_idx", + "columns": [ + { + "expression": "collectedByUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_placementId_idx": { + "name": "SatisfactionCheckIn_placementId_idx", + "columns": [ + { + "expression": "placementId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "SatisfactionCheckIn_placementId_fkey": { + "name": "SatisfactionCheckIn_placementId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Placement", + "columnsFrom": ["placementId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_appointmentId_fkey": { + "name": "SatisfactionCheckIn_appointmentId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Appointment", + "columnsFrom": ["appointmentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_collectedByUserId_fkey": { + "name": "SatisfactionCheckIn_collectedByUserId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "User", + "columnsFrom": ["collectedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Settlement": { + "name": "Settlement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromId": { + "name": "fromId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toId": { + "name": "toId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Settlement_housingUnitId_idx": { + "name": "Settlement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Settlement_housingUnitId_fkey": { + "name": "Settlement_housingUnitId_fkey", + "tableFrom": "Settlement", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Settlement_fromId_fkey": { + "name": "Settlement_fromId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": ["fromId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Settlement_toId_fkey": { + "name": "Settlement_toId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": ["toId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.StaffUnit": { + "name": "StaffUnit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "StaffUnit_staffId_housingUnitId_key": { + "name": "StaffUnit_staffId_housingUnitId_key", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "StaffUnit_housingUnitId_idx": { + "name": "StaffUnit_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "StaffUnit_staffId_fkey": { + "name": "StaffUnit_staffId_fkey", + "tableFrom": "StaffUnit", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "StaffUnit_housingUnitId_fkey": { + "name": "StaffUnit_housingUnitId_fkey", + "tableFrom": "StaffUnit", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SystemConfig": { + "name": "SystemConfig", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'singleton'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "pilotBaselineIncidentsPerMonth": { + "name": "pilotBaselineIncidentsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineRelocationsPerMonth": { + "name": "pilotBaselineRelocationsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineMediationHoursPerWeek": { + "name": "pilotBaselineMediationHoursPerWeek", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotStartDate": { + "name": "pilotStartDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskAttentionFlag": { + "name": "TaskAttentionFlag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flaggedById": { + "name": "flaggedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isResolved": { + "name": "isResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolvedByCompletionId": { + "name": "resolvedByCompletionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskAttentionFlag_taskId_idx": { + "name": "TaskAttentionFlag_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskAttentionFlag_taskId_fkey": { + "name": "TaskAttentionFlag_taskId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_flaggedById_fkey": { + "name": "TaskAttentionFlag_flaggedById_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "Resident", + "columnsFrom": ["flaggedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_resolvedByCompletionId_fkey": { + "name": "TaskAttentionFlag_resolvedByCompletionId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "TaskCompletion", + "columnsFrom": ["resolvedByCompletionId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskCompletion": { + "name": "TaskCompletion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedById": { + "name": "completedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "durationMinutes": { + "name": "durationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completedItems": { + "name": "completedItems", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "TaskCompletion_completedById_idx": { + "name": "TaskCompletion_completedById_idx", + "columns": [ + { + "expression": "completedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskCompletion_taskId_idx": { + "name": "TaskCompletion_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskCompletion_taskId_fkey": { + "name": "TaskCompletion_taskId_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskCompletion_completedById_fkey": { + "name": "TaskCompletion_completedById_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "Resident", + "columnsFrom": ["completedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskRequest": { + "name": "TaskRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedById": { + "name": "requestedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedResidentId": { + "name": "requestedResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isBroadcast": { + "name": "isBroadcast", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "TaskRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "responseMessage": { + "name": "responseMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completionId": { + "name": "completionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskRequest_requestedResidentId_idx": { + "name": "TaskRequest_requestedResidentId_idx", + "columns": [ + { + "expression": "requestedResidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskRequest_taskId_idx": { + "name": "TaskRequest_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskRequest_taskId_fkey": { + "name": "TaskRequest_taskId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedById_fkey": { + "name": "TaskRequest_requestedById_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": ["requestedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedResidentId_fkey": { + "name": "TaskRequest_requestedResidentId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": ["requestedResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TaskRequest_completionId_fkey": { + "name": "TaskRequest_completionId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "TaskCompletion", + "columnsFrom": ["completionId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TransferRequest": { + "name": "TransferRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currentPlacementId": { + "name": "currentPlacementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "targetUnitId": { + "name": "targetUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "TransferRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedBy": { + "name": "reviewedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TransferRequest_residentId_idx": { + "name": "TransferRequest_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TransferRequest_status_idx": { + "name": "TransferRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TransferRequest_residentId_fkey": { + "name": "TransferRequest_residentId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TransferRequest_currentPlacementId_fkey": { + "name": "TransferRequest_currentPlacementId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Placement", + "columnsFrom": ["currentPlacementId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TransferRequest_targetUnitId_fkey": { + "name": "TransferRequest_targetUnitId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "HousingUnit", + "columnsFrom": ["targetUnitId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "StaffRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'BETREUUNG'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "lastLoginAt": { + "name": "lastLoginAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "StaffScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OWN_DOMAIN'" + }, + "isSystemAdmin": { + "name": "isSystemAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "siteAccess": { + "name": "siteAccess", + "type": "SiteAccess", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ALL_UNITS'" + } + }, + "indexes": { + "User_code_idx": { + "name": "User_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_role_idx": { + "name": "User_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_scope_idx": { + "name": "User_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_code_key": { + "name": "User_code_key", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Vote": { + "name": "Vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "proposalId": { + "name": "proposalId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "choice": { + "name": "choice", + "type": "VoteChoice", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "castAt": { + "name": "castAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Vote_proposalId_residentId_key": { + "name": "Vote_proposalId_residentId_key", + "columns": [ + { + "expression": "proposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Vote_residentId_idx": { + "name": "Vote_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Vote_proposalId_fkey": { + "name": "Vote_proposalId_fkey", + "tableFrom": "Vote", + "tableTo": "Proposal", + "columnsFrom": ["proposalId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Vote_residentId_fkey": { + "name": "Vote_residentId_fkey", + "tableFrom": "Vote", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ActivityCategory": { + "name": "ActivityCategory", + "schema": "public", + "values": ["SPORT", "LANGUAGE", "CULTURE", "COMMUNITY", "FAMILY", "SUPPORT"] + }, + "public.ActivityCost": { + "name": "ActivityCost", + "schema": "public", + "values": ["FREE", "REDUCED", "PAID"] + }, + "public.ActivityStatus": { + "name": "ActivityStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] + }, + "public.AgeRange": { + "name": "AgeRange", + "schema": "public", + "values": ["YOUNG_ADULT", "ADULT", "MIDDLE_AGED", "SENIOR"] + }, + "public.AgreementStatus": { + "name": "AgreementStatus", + "schema": "public", + "values": ["PROPOSED", "ACCEPTED", "HELD", "BROKEN", "EXPIRED"] + }, + "public.ApplicationStage": { + "name": "ApplicationStage", + "schema": "public", + "values": ["INTERESTED", "APPLIED", "INTERVIEW", "ACCEPTED", "STARTED", "ENDED", "DECLINED"] + }, + "public.AppointmentStatus": { + "name": "AppointmentStatus", + "schema": "public", + "values": ["SCHEDULED", "COMPLETED", "CANCELLED", "NO_SHOW", "REQUESTED"] + }, + "public.AuthTokenPurpose": { + "name": "AuthTokenPurpose", + "schema": "public", + "values": ["VERIFY_EMAIL", "RESET_PASSWORD"] + }, + "public.CareRole": { + "name": "CareRole", + "schema": "public", + "values": ["HOUSING", "SOCIAL", "JOB", "VOLUNTEERING"] + }, + "public.CheckInType": { + "name": "CheckInType", + "schema": "public", + "values": ["INITIAL", "REGULAR", "AD_HOC", "EXIT"] + }, + "public.ComplaintStatus": { + "name": "ComplaintStatus", + "schema": "public", + "values": ["OPEN", "IN_REVIEW", "ANSWERED"] + }, + "public.ComplaintSubject": { + "name": "ComplaintSubject", + "schema": "public", + "values": ["STAFF", "ACCOMMODATION", "DECISION", "OTHER"] + }, + "public.ConflictStyle": { + "name": "ConflictStyle", + "schema": "public", + "values": ["AVOIDANT", "COOPERATIVE", "DIRECT"] + }, + "public.DecisionMode": { + "name": "DecisionMode", + "schema": "public", + "values": ["RESIDENT_BINDING", "RESIDENT_ADVISORY", "STAFF_ONLY"] + }, + "public.EndReason": { + "name": "EndReason", + "schema": "public", + "values": ["NATURAL", "CONFLICT", "REQUEST", "CAPACITY", "UPGRADE", "OTHER"] + }, + "public.EventRsvpStatus": { + "name": "EventRsvpStatus", + "schema": "public", + "values": ["GOING", "MAYBE", "DECLINED"] + }, + "public.FamilyStatus": { + "name": "FamilyStatus", + "schema": "public", + "values": ["SINGLE", "COUPLE", "FAMILY_WITH_CHILDREN", "SINGLE_PARENT"] + }, + "public.FollowUpPriority": { + "name": "FollowUpPriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.Gender": { + "name": "Gender", + "schema": "public", + "values": ["MALE", "FEMALE", "OTHER", "PREFER_NOT_SAY"] + }, + "public.HouseEventCategory": { + "name": "HouseEventCategory", + "schema": "public", + "values": ["HOUSE_MEETING", "SOCIAL", "CULTURE", "SUPPORT"] + }, + "public.HouseEventStatus": { + "name": "HouseEventStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "CANCELLED"] + }, + "public.HouseholdTaskCategory": { + "name": "HouseholdTaskCategory", + "schema": "public", + "values": ["CLEANING", "SHOPPING", "MAINTENANCE", "COOKING", "TRASH", "OTHER"] + }, + "public.HouseholdTaskPriority": { + "name": "HouseholdTaskPriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.HouseholdTaskStatus": { + "name": "HouseholdTaskStatus", + "schema": "public", + "values": ["IDLE", "NEEDS_ATTENTION", "REQUESTED", "IN_PROGRESS"] + }, + "public.HouseholdTaskType": { + "name": "HouseholdTaskType", + "schema": "public", + "values": ["ONE_TIME", "RECURRING_SCHEDULED", "RECURRING_AS_NEEDED"] + }, + "public.HousingStatus": { + "name": "HousingStatus", + "schema": "public", + "values": ["AVAILABLE", "FULL", "MAINTENANCE", "CLOSED"] + }, + "public.IncidentCategory": { + "name": "IncidentCategory", + "schema": "public", + "values": ["INTERPERSONAL", "MAINTENANCE", "SAFETY", "WELLBEING"] + }, + "public.IncidentSeverity": { + "name": "IncidentSeverity", + "schema": "public", + "values": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "public.IncidentType": { + "name": "IncidentType", + "schema": "public", + "values": [ + "NOISE_COMPLAINT", + "CLEANLINESS_DISPUTE", + "PERSONAL_CONFLICT", + "CULTURAL_FRICTION", + "SPACE_DISPUTE", + "SCHEDULE_CONFLICT", + "SAFETY_CONCERN", + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY_SYSTEM", + "GENERAL_MAINTENANCE", + "LOW_SATISFACTION", + "OTHER" + ] + }, + "public.InvolvementRole": { + "name": "InvolvementRole", + "schema": "public", + "values": ["INVOLVED", "WITNESS", "MEDIATOR"] + }, + "public.LearningKind": { + "name": "LearningKind", + "schema": "public", + "values": [ + "LANGUAGE_TEST", + "COURSE", + "INFORMAL", + "QUALIFICATION", + "VOLUNTEERING", + "COMMUNITY_SERVICE", + "EMPLOYMENT", + "INTERNSHIP" + ] + }, + "public.LearningStatus": { + "name": "LearningStatus", + "schema": "public", + "values": ["PLANNED", "IN_PROGRESS", "COMPLETED", "EXPIRED"] + }, + "public.LivingSkillsSupport": { + "name": "LivingSkillsSupport", + "schema": "public", + "values": ["INDEPENDENT", "SOME_SUPPORT", "REGULAR_SUPPORT"] + }, + "public.MaintenanceCategory": { + "name": "MaintenanceCategory", + "schema": "public", + "values": [ + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY", + "CLEANING", + "EXTERIOR", + "OTHER" + ] + }, + "public.MaintenancePriority": { + "name": "MaintenancePriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.MaintenanceStatus": { + "name": "MaintenanceStatus", + "schema": "public", + "values": ["OPEN", "ASSIGNED", "IN_PROGRESS", "ON_HOLD", "COMPLETED", "CANCELLED"] + }, + "public.MarketplacePostKind": { + "name": "MarketplacePostKind", + "schema": "public", + "values": ["GIVE_AWAY", "LEND", "WANTED", "OFFER_HELP", "NEED_HELP"] + }, + "public.MarketplacePostStatus": { + "name": "MarketplacePostStatus", + "schema": "public", + "values": ["OPEN", "CLAIMED", "CLOSED"] + }, + "public.MedicalDocType": { + "name": "MedicalDocType", + "schema": "public", + "values": ["PRIVATE_ROOM", "STUDIO", "BOTH"] + }, + "public.MobilityNeed": { + "name": "MobilityNeed", + "schema": "public", + "values": ["NONE", "GROUND_FLOOR", "WHEELCHAIR"] + }, + "public.OpportunityKind": { + "name": "OpportunityKind", + "schema": "public", + "values": ["VOLUNTEERING", "COMMUNITY_SERVICE", "EMPLOYMENT", "INTERNSHIP"] + }, + "public.OpportunityStatus": { + "name": "OpportunityStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] + }, + "public.PermitRequirement": { + "name": "PermitRequirement", + "schema": "public", + "values": ["NONE", "EMPLOYER_NOTIFIES", "PERMIT_REQUIRED"] + }, + "public.PlacementStatus": { + "name": "PlacementStatus", + "schema": "public", + "values": ["ACTIVE", "ENDED", "TRANSFERRED"] + }, + "public.ProfileVisibility": { + "name": "ProfileVisibility", + "schema": "public", + "values": ["PRIVATE", "ROOMMATES", "RESIDENTS"] + }, + "public.ProposalStatus": { + "name": "ProposalStatus", + "schema": "public", + "values": [ + "DISCUSSION", + "VOTING", + "NEEDS_STAFF_CONFIRMATION", + "ACCEPTED", + "REJECTED", + "WITHDRAWN", + "VETOED", + "EXPIRED" + ] + }, + "public.ProposalType": { + "name": "ProposalType", + "schema": "public", + "values": ["ADD_RULE", "AMEND_RULE", "REPEAL_RULE", "HOUSE_DECISION"] + }, + "public.RecyclingKnowledge": { + "name": "RecyclingKnowledge", + "schema": "public", + "values": ["NONE", "BASIC", "GOOD"] + }, + "public.ResidentOrStaff": { + "name": "ResidentOrStaff", + "schema": "public", + "values": ["RESIDENT", "STAFF"] + }, + "public.ResidentStatus": { + "name": "ResidentStatus", + "schema": "public", + "values": ["ACTIVE", "PLACED", "TRANSFERRED", "EXITED"] + }, + "public.ResolutionStage": { + "name": "ResolutionStage", + "schema": "public", + "values": [ + "REPORTED", + "SELF_RESOLUTION", + "PEER_MEDIATION", + "STAFF_MEDIATION", + "FORMAL_MEASURE", + "CLOSED" + ] + }, + "public.RoomSharingStatus": { + "name": "RoomSharingStatus", + "schema": "public", + "values": ["CAN_SHARE", "PREFERS_PRIVATE", "NEEDS_PRIVATE"] + }, + "public.RuleCategory": { + "name": "RuleCategory", + "schema": "public", + "values": [ + "SAFETY", + "RESPECT", + "NOISE", + "CLEANLINESS", + "KITCHEN", + "BATHROOM", + "GUESTS", + "SHARED_SPACES", + "COSTS", + "COMMUNICATION", + "OTHER" + ] + }, + "public.RuleDelegation": { + "name": "RuleDelegation", + "schema": "public", + "values": ["FIXED", "UNIT_MAY_STRENGTHEN", "UNIT_DECIDES"] + }, + "public.RuleScope": { + "name": "RuleScope", + "schema": "public", + "values": ["ORG", "UNIT"] + }, + "public.RuleStatus": { + "name": "RuleStatus", + "schema": "public", + "values": ["ACTIVE", "SUPERSEDED", "ARCHIVED"] + }, + "public.SiteAccess": { + "name": "SiteAccess", + "schema": "public", + "values": ["ALL_UNITS", "ASSIGNED_UNITS"] + }, + "public.SleepSchedule": { + "name": "SleepSchedule", + "schema": "public", + "values": ["EARLY_BIRD", "STANDARD", "NIGHT_OWL", "IRREGULAR"] + }, + "public.SmokingStatus": { + "name": "SmokingStatus", + "schema": "public", + "values": ["NON_SMOKER", "OUTDOOR_SMOKER", "INDOOR_SMOKER"] + }, + "public.SocialStyle": { + "name": "SocialStyle", + "schema": "public", + "values": ["INTROVERTED", "MODERATE", "EXTROVERTED"] + }, + "public.SpotStatus": { + "name": "SpotStatus", + "schema": "public", + "values": ["AVAILABLE", "OCCUPIED", "MAINTENANCE", "CLOSED"] + }, + "public.SpotType": { + "name": "SpotType", + "schema": "public", + "values": ["BED", "PRIVATE_ROOM", "STUDIO", "ROOM"] + }, + "public.StaffDecision": { + "name": "StaffDecision", + "schema": "public", + "values": ["CONFIRMED", "VETOED"] + }, + "public.StaffRole": { + "name": "StaffRole", + "schema": "public", + "values": ["ADMIN", "BETREUUNG", "SOZIALARBEIT", "JOBCOACH", "FREIWILLIGENARBEIT"] + }, + "public.StaffScope": { + "name": "StaffScope", + "schema": "public", + "values": ["OWN_DOMAIN", "ALL_DOMAINS"] + }, + "public.SupportLevel": { + "name": "SupportLevel", + "schema": "public", + "values": ["STANDARD", "ELEVATED", "INTENSIVE"] + }, + "public.TaskRequestStatus": { + "name": "TaskRequestStatus", + "schema": "public", + "values": ["PENDING", "ACCEPTED", "DECLINED", "COMPLETED"] + }, + "public.TransferRequestStatus": { + "name": "TransferRequestStatus", + "schema": "public", + "values": ["PENDING", "APPROVED", "DENIED", "COMPLETED", "CANCELLED"] + }, + "public.VoteChoice": { + "name": "VoteChoice", + "schema": "public", + "values": ["YES", "NO", "ABSTAIN", "BLOCK"] + }, + "public.VoteThreshold": { + "name": "VoteThreshold", + "schema": "public", + "values": ["CONSENSUS", "SUPERMAJORITY", "SIMPLE_MAJORITY"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index fc9b9d9f..1c1874bb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1788303042211, "tag": "0000_unusual_steel_serpent", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1788340236588, + "tag": "0001_staff_site_access", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/lib/auth/__tests__/current-user.test.ts b/src/lib/auth/__tests__/current-user.test.ts index b561d999..82f1ca67 100644 --- a/src/lib/auth/__tests__/current-user.test.ts +++ b/src/lib/auth/__tests__/current-user.test.ts @@ -19,11 +19,15 @@ jest.mock('../jwt', () => ({ refreshToken: jest.fn(), })) -const mockUserFindUnique = jest.fn() +const mockUserFindFirst = jest.fn() jest.mock('@/lib/db', () => ({ - prisma: { - user: { findUnique: (...args: unknown[]) => mockUserFindUnique(...args) }, - resident: { findUnique: jest.fn() }, + // Keep the real tables/enums/helpers; fake only the client. + ...jest.requireActual('@/lib/db'), + db: { + query: { + user: { findFirst: (...args: unknown[]) => mockUserFindFirst(...args) }, + resident: { findFirst: jest.fn() }, + }, }, })) @@ -43,7 +47,7 @@ describe('getCurrentUser', () => { // be `{ active: true }` alone, which passed only because the function read // nothing else off the row — every field added since is a fact this test // should be asserting travels. - mockUserFindUnique.mockResolvedValue({ + mockUserFindFirst.mockResolvedValue({ active: true, scope: 'ALL_DOMAINS', isSystemAdmin: true, @@ -68,7 +72,7 @@ describe('getCurrentUser', () => { // come from the ROW for the same reason `scope` does: a privilege in a JWT // goes stale, and with sliding refresh "stale" means indefinitely — // revoking somebody's reach has to take effect on the next request. - mockUserFindUnique.mockResolvedValue({ + mockUserFindFirst.mockResolvedValue({ active: true, scope: 'OWN_DOMAIN', isSystemAdmin: false, @@ -85,7 +89,7 @@ describe('getCurrentUser', () => { // Impossible in production — the column is NOT NULL with a default — but // this is the auth path. An incomplete select must narrow someone's reach, // never take the request down. - mockUserFindUnique.mockResolvedValue({ + mockUserFindFirst.mockResolvedValue({ active: true, scope: 'OWN_DOMAIN', isSystemAdmin: false, @@ -97,24 +101,24 @@ describe('getCurrentUser', () => { }) it('rejects a valid token whose account was deactivated', async () => { - mockUserFindUnique.mockResolvedValue({ active: false }) + mockUserFindFirst.mockResolvedValue({ active: false }) expect(await getCurrentUser()).toBeNull() }) it('rejects a valid token whose account no longer exists', async () => { - mockUserFindUnique.mockResolvedValue(null) + mockUserFindFirst.mockResolvedValue(null) expect(await getCurrentUser()).toBeNull() }) it('returns null without a cookie', async () => { mockCookieGet.mockReturnValue(undefined) expect(await getCurrentUser()).toBeNull() - expect(mockUserFindUnique).not.toHaveBeenCalled() + expect(mockUserFindFirst).not.toHaveBeenCalled() }) it('returns null for an invalid token without touching the database', async () => { mockVerifyToken.mockResolvedValue(null) expect(await getCurrentUser()).toBeNull() - expect(mockUserFindUnique).not.toHaveBeenCalled() + expect(mockUserFindFirst).not.toHaveBeenCalled() }) }) diff --git a/src/lib/auth/__tests__/site-access.test.ts b/src/lib/auth/__tests__/site-access.test.ts index ed8aed1a..8a44fc73 100644 --- a/src/lib/auth/__tests__/site-access.test.ts +++ b/src/lib/auth/__tests__/site-access.test.ts @@ -1,5 +1,8 @@ import fs from 'fs' import path from 'path' +import { and, eq, inArray, sql } from 'drizzle-orm' +import { PgDialect, QueryBuilder } from 'drizzle-orm/pg-core' +import { housingUnit, incident, placement, resident, user } from '@/lib/db' import { canAccessUnit, housingUnitScopeFilter, @@ -29,25 +32,24 @@ describe('nobody loses access on the day this ships', () => { */ it('an ALL_UNITS viewer is unrestricted everywhere', () => { expect(unitScopeFilter(everywhere)).toBeNull() - expect(housingUnitScopeFilter(everywhere)).toBeNull() + expect(housingUnitScopeFilter(everywhere, incident.housingUnitId)).toBeNull() expect(residentScopeFilter(everywhere)).toBeNull() expect(canAccessUnit(everywhere, 'any-unit-at-all')).toBe(true) }) it('returns null rather than an always-true clause', () => { - // `...(filter ?? {})` must add NOTHING for the common viewer, so the query - // Prisma issues is byte-identical to the one before this axis existed. An - // `{ id: { in: [...everything] } }` would have been correct and would also + // `and(filter ?? undefined, …)` must add NOTHING for the common viewer, so + // the query issued is identical to the one before this axis existed. An + // `inArray(id, [...everything])` would have been correct and would also // have quietly changed every query plan in the product. expect(unitScopeFilter(everywhere)).toBeNull() }) it('the schema default is ALL_UNITS, not the restrictive value', () => { - const schema = fs.readFileSync( - path.resolve(__dirname, '../../../../prisma/schema.prisma'), - 'utf8', - ) - expect(schema).toMatch(/siteAccess\s+SiteAccess\s+@default\(ALL_UNITS\)/) + // Straight off the drizzle column definition — the schema IS the runtime + // object, so no file parsing is needed. + expect(user.siteAccess.hasDefault).toBe(true) + expect(user.siteAccess.default).toBe('ALL_UNITS') }) }) @@ -58,25 +60,40 @@ describe('a restricted viewer sees only their places', () => { }) it('filters units by id', () => { - expect(unitScopeFilter(twoHouses)).toEqual({ id: { in: ['unit-a', 'unit-b'] } }) + expect(unitScopeFilter(twoHouses)).toEqual(inArray(housingUnit.id, ['unit-a', 'unit-b'])) }) it('filters unit-owned rows by housingUnitId', () => { // Placements, incidents and maintenance all point AT a unit rather than - // being one, so they need the other column name. - expect(housingUnitScopeFilter(twoHouses)).toEqual({ - housingUnitId: { in: ['unit-a', 'unit-b'] }, - }) + // being one, so the caller names its table's column. + expect(housingUnitScopeFilter(twoHouses, incident.housingUnitId)).toEqual( + inArray(incident.housingUnitId, ['unit-a', 'unit-b']), + ) }) it('scopes residents by their ACTIVE placement, not any placement', () => { // Load-bearing. Matching any placement would leak everyone who ever passed // through a house the viewer covers — including people who have since // moved somewhere the viewer does not cover. - const filter = residentScopeFilter(twoHouses) - expect(filter).toEqual({ - placements: { some: { status: 'ACTIVE', housingUnitId: { in: ['unit-a', 'unit-b'] } } }, - }) + const expected = inArray( + resident.id, + new QueryBuilder() + .select({ id: placement.residentId }) + .from(placement) + .where( + and( + eq(placement.status, 'ACTIVE'), + inArray(placement.housingUnitId, ['unit-a', 'unit-b']), + ), + ), + ) + // Compared as rendered SQL + params: the two expression trees carry + // internal closures jest cannot deep-compare, but what the database sees + // is exactly this pair. + const dialect = new PgDialect() + const got = residentScopeFilter(twoHouses) + expect(got).not.toBeNull() + expect(dialect.sqlToQuery(got as NonNullable)).toEqual(dialect.sqlToQuery(expected)) }) }) @@ -84,7 +101,7 @@ describe('a restricted viewer with no units is misconfigured, not idle', () => { /** * This product has already made the neighbouring mistake once: a specialist * with nobody assigned was shown "🎉 Alles unter Kontrolle!" on their first - * login. An `{ in: [] }` filter is silently and permanently empty, and looks + * login. An empty-list filter is silently and permanently empty, and looks * exactly like a quiet day. */ it('is detectable rather than indistinguishable from an empty day', () => { @@ -95,8 +112,9 @@ describe('a restricted viewer with no units is misconfigured, not idle', () => { it('still produces a filter that matches nothing, rather than everything', () => { // The dangerous failure would be treating "no units assigned" as "no - // restriction". Fail closed. - expect(unitScopeFilter(stranded)).toEqual({ id: { in: [] } }) + // restriction". Fail closed. (drizzle's `inArray` throws on an empty + // list, so the helper renders literal FALSE instead.) + expect(unitScopeFilter(stranded)).toEqual(sql`false`) expect(canAccessUnit(stranded, 'unit-a')).toBe(false) }) }) @@ -109,18 +127,19 @@ describe('the boards that enforce it', () => { // Scoping only the list would leave a restricted viewer reading counts // that describe houses they cannot open — the count is the leak. const source = fs.readFileSync(path.join(ADMIN_DIR, 'housing/page.tsx'), 'utf8') - const queries = source.match(/prisma\.housingUnit\.findMany\(/g) ?? [] - const scoped = source.match(/\.\.\.\(unitFilter \?\? \{\}\)/g) ?? [] + const queries = source.match(/db\.query\.housingUnit\.findMany\(/g) ?? [] + const scoped = source.match(/unitFilter \?\? undefined/g) ?? [] expect({ queries: queries.length, scoped: scoped.length }).toEqual({ queries: queries.length, scoped: queries.length, }) + expect(queries.length).toBeGreaterThanOrEqual(2) }) it('the resident board scopes its list', () => { const source = fs.readFileSync(path.join(ADMIN_DIR, 'residents/page.tsx'), 'utf8') expect(source).toMatch(/siteFilter\s*=\s*currentUser\s*\?\s*residentScopeFilter\(currentUser\)/) - expect(source).toMatch(/\.\.\.\(siteFilter \?\? \{\}\)/) + expect(source).toMatch(/siteFilter \?\? undefined/) }) }) @@ -135,7 +154,9 @@ describe('site access is read from the row, never the token', () => { source.indexOf('export async function getTokenPayload'), ) expect(currentUser).toMatch(/siteAccess:\s*true/) - expect(currentUser).toMatch(/unitAccess:\s*\{\s*select:\s*\{\s*housingUnitId:\s*true\s*\}\s*\}/) + expect(currentUser).toMatch( + /unitAccess:\s*\{\s*columns:\s*\{\s*housingUnitId:\s*true\s*\}\s*\}/, + ) expect(currentUser).not.toMatch(/payload\.siteAccess/) }) }) diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index da24217a..0756cb6d 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -113,9 +113,7 @@ export async function getCurrentUser(): Promise { // production — but this is the auth path, and an incomplete select should // narrow someone's reach, never throw and take the whole request down. assignedUnitIds: - row.siteAccess === 'ALL_UNITS' - ? [] - : (row.unitAccess ?? []).map((r) => r.housingUnitId), + row.siteAccess === 'ALL_UNITS' ? [] : (row.unitAccess ?? []).map((r) => r.housingUnitId), } } diff --git a/src/lib/auth/site-access.ts b/src/lib/auth/site-access.ts index f41e52b3..aae5dd92 100644 --- a/src/lib/auth/site-access.ts +++ b/src/lib/auth/site-access.ts @@ -27,7 +27,16 @@ * seat, this decides the address. */ -import type { SiteAccess } from '@prisma/client' +import { and, eq, inArray, sql } from 'drizzle-orm' +import type { SQL } from 'drizzle-orm' +import { QueryBuilder, type PgColumn } from 'drizzle-orm/pg-core' +import { housingUnit, placement, resident } from '@/lib/db' +import type { SiteAccess } from '@/lib/db' + +// A standalone builder: these helpers only CONSTRUCT where-fragments, and must +// do so without touching the lazy db client (jest and next build both call +// them in environments where DATABASE_URL is absent). +const qb = new QueryBuilder() /** Everything a site question needs about the person asking. */ export interface SiteCapabilities { @@ -36,6 +45,15 @@ export interface SiteCapabilities { assignedUnitIds: readonly string[] } +/** + * `inArray` throws on an empty list, and "assigned nowhere" must read as + * matching NOTHING (see isStrandedWithoutUnits) — never as an error and never + * as everything. + */ +function inIds(column: PgColumn, ids: readonly string[]): SQL { + return ids.length > 0 ? inArray(column, [...ids]) : sql`false` +} + /** * Whether this person may see anything at all about a given unit. * @@ -48,36 +66,40 @@ export function canAccessUnit(viewer: SiteCapabilities, housingUnitId: string): } /** - * A Prisma `where` fragment restricting rows to this viewer's units, or `null` - * when no restriction applies. + * A `where` fragment restricting HousingUnit rows to this viewer's units, or + * `null` when no restriction applies. * * Returning null rather than an always-true clause is deliberate: a caller - * that spreads `...(unitFilter ?? {})` adds nothing for an ALL_UNITS viewer, so - * the common path issues exactly the query it issued before this axis existed. + * that passes `and(unitFilter ?? undefined, …)` adds nothing for an ALL_UNITS + * viewer, so the common path issues exactly the query it issued before this + * axis existed. */ -export function unitScopeFilter(viewer: SiteCapabilities): { id: { in: string[] } } | null { +export function unitScopeFilter(viewer: SiteCapabilities): SQL | null { if (viewer.siteAccess === 'ALL_UNITS') return null - return { id: { in: [...viewer.assignedUnitIds] } } + return inIds(housingUnit.id, viewer.assignedUnitIds) } /** * The same restriction expressed for rows that POINT AT a unit — placements, - * incidents, maintenance — where the column is `housingUnitId`. + * incidents, maintenance — where the column is `housingUnitId`. The caller + * names its table's column, because each drizzle table carries its own. */ export function housingUnitScopeFilter( viewer: SiteCapabilities, -): { housingUnitId: { in: string[] } } | null { + housingUnitIdColumn: PgColumn, +): SQL | null { if (viewer.siteAccess === 'ALL_UNITS') return null - return { housingUnitId: { in: [...viewer.assignedUnitIds] } } + return inIds(housingUnitIdColumn, viewer.assignedUnitIds) } /** - * Residents this viewer may see, as a Prisma `where` fragment. + * Residents this viewer may see, as a `where` fragment on Resident. * * A resident is in scope when they hold an ACTIVE placement in one of the - * viewer's units. The `some` is load-bearing: matching on any placement would - * leak everyone who ever passed through a house the viewer covers, including - * people who have since moved somewhere they do not. + * viewer's units — Prisma's `placements: { some: … }`, expressed as a + * subquery. The ACTIVE condition is load-bearing: matching on any placement + * would leak everyone who ever passed through a house the viewer covers, + * including people who have since moved somewhere they do not. * * Unplaced residents are deliberately NOT in scope for a restricted viewer. * Somebody who has not been placed anywhere belongs to no site, so there is no @@ -85,15 +107,17 @@ export function housingUnitScopeFilter( * site-restricted specialist does not do. This is the one case worth * re-examining if AOZ ever restricts a Betreuer who also does intake. */ -export function residentScopeFilter(viewer: SiteCapabilities): { - placements: { some: { status: 'ACTIVE'; housingUnitId: { in: string[] } } } -} | null { +export function residentScopeFilter(viewer: SiteCapabilities): SQL | null { if (viewer.siteAccess === 'ALL_UNITS') return null - return { - placements: { - some: { status: 'ACTIVE', housingUnitId: { in: [...viewer.assignedUnitIds] } }, - }, - } + return inArray( + resident.id, + qb + .select({ id: placement.residentId }) + .from(placement) + .where( + and(eq(placement.status, 'ACTIVE'), inIds(placement.housingUnitId, viewer.assignedUnitIds)), + ), + ) } /** @@ -101,7 +125,7 @@ export function residentScopeFilter(viewer: SiteCapabilities): { * misconfiguration, not a state to render as "all quiet". * * Same failure the dashboard already learned once: a specialist with no clients - * was congratulated on an empty day. An `{ in: [] }` filter is silently and + * was congratulated on an empty day. An empty filter is silently and * permanently empty, so the surfaces that use it must be able to say WHY. */ export function isStrandedWithoutUnits(viewer: SiteCapabilities): boolean { diff --git a/src/lib/db/relations.ts b/src/lib/db/relations.ts index e1bdd549..6a2572c6 100644 --- a/src/lib/db/relations.ts +++ b/src/lib/db/relations.ts @@ -7,691 +7,705 @@ */ import { relations } from 'drizzle-orm/relations' import { - account, - activity, - agreementParty, - appointment, - auditLog, - authToken, - careAssignment, - careAttribute, - compatibilityAssessment, - complaint, - conflictAgreement, - eventRsvp, - expense, - expenseShare, - houseEvent, - householdTask, - houseRule, - housingUnit, - incident, - incidentFollowUp, - incidentInvolvement, - learningRecord, - maintenanceRequest, - marketplacePost, - message, - messageThread, - opportunity, - opportunityApplication, - placement, - placementSpot, - proposal, - resident, - residentDocument, - residentDocumentBlob, - residentPhoto, - ruleAcknowledgement, - satisfactionCheckIn, - settlement, - taskAttentionFlag, - taskCompletion, - taskRequest, - transferRequest, - user, - vote, + account, + activity, + agreementParty, + appointment, + auditLog, + authToken, + careAssignment, + careAttribute, + compatibilityAssessment, + complaint, + conflictAgreement, + eventRsvp, + expense, + expenseShare, + houseEvent, + householdTask, + houseRule, + housingUnit, + incident, + incidentFollowUp, + incidentInvolvement, + learningRecord, + maintenanceRequest, + marketplacePost, + message, + messageThread, + opportunity, + opportunityApplication, + placement, + placementSpot, + proposal, + resident, + residentDocument, + residentDocumentBlob, + residentPhoto, + ruleAcknowledgement, + satisfactionCheckIn, + settlement, + staffUnit, + taskAttentionFlag, + taskCompletion, + taskRequest, + transferRequest, + user, + vote, } from './schema' export const residentRelations = relations(resident, ({ one, many }) => ({ - photo: one(residentPhoto), - documents: many(residentDocument), - complaints: many(complaint), - messageThread: one(messageThread), - messagesWritten: many(message, { relationName: 'MessageAuthor' }), - account: one(account), - placements: many(placement), - assessments: many(compatibilityAssessment, { relationName: 'ResidentAssessments' }), - comparedWith: many(compatibilityAssessment, { relationName: 'ComparedResidentAssessments' }), - incidentsReported: many(incident, { relationName: 'IncidentReporter' }), - incidentsAsSubject: many(incident, { relationName: 'IncidentSubject' }), - incidentInvolvements: many(incidentInvolvement), - maintenanceRequests: many(maintenanceRequest), - createdTasks: many(householdTask, { relationName: 'TaskCreator' }), - taskCompletions: many(taskCompletion), - taskAttentionFlags: many(taskAttentionFlag), - taskRequestsMade: many(taskRequest, { relationName: 'TaskRequestsMade' }), - taskRequestsReceived: many(taskRequest, { relationName: 'TaskRequestsReceived' }), - transferRequests: many(transferRequest), - ruleAcknowledgements: many(ruleAcknowledgement), - proposalsMade: many(proposal, { relationName: 'ProposalAuthor' }), - votes: many(vote), - agreementParties: many(agreementParty), - expensesPaid: many(expense, { relationName: 'ExpensePayer' }), - expensesCreated: many(expense, { relationName: 'ExpenseCreator' }), - expenseShares: many(expenseShare), - settlementsPaid: many(settlement, { relationName: 'SettlementFrom' }), - settlementsRecvd: many(settlement, { relationName: 'SettlementTo' }), - learningRecords: many(learningRecord), - careAssignments: many(careAssignment), - appointments: many(appointment), - careAttributes: many(careAttribute), - opportunityApplications: many(opportunityApplication), - marketplacePostsCreated: many(marketplacePost, { relationName: 'MarketplacePostedBy' }), - marketplacePostsClaimed: many(marketplacePost, { relationName: 'MarketplacePostClaimedBy' }), - houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByResident' }), - eventRsvps: many(eventRsvp), + photo: one(residentPhoto), + documents: many(residentDocument), + complaints: many(complaint), + messageThread: one(messageThread), + messagesWritten: many(message, { relationName: 'MessageAuthor' }), + account: one(account), + placements: many(placement), + assessments: many(compatibilityAssessment, { relationName: 'ResidentAssessments' }), + comparedWith: many(compatibilityAssessment, { relationName: 'ComparedResidentAssessments' }), + incidentsReported: many(incident, { relationName: 'IncidentReporter' }), + incidentsAsSubject: many(incident, { relationName: 'IncidentSubject' }), + incidentInvolvements: many(incidentInvolvement), + maintenanceRequests: many(maintenanceRequest), + createdTasks: many(householdTask, { relationName: 'TaskCreator' }), + taskCompletions: many(taskCompletion), + taskAttentionFlags: many(taskAttentionFlag), + taskRequestsMade: many(taskRequest, { relationName: 'TaskRequestsMade' }), + taskRequestsReceived: many(taskRequest, { relationName: 'TaskRequestsReceived' }), + transferRequests: many(transferRequest), + ruleAcknowledgements: many(ruleAcknowledgement), + proposalsMade: many(proposal, { relationName: 'ProposalAuthor' }), + votes: many(vote), + agreementParties: many(agreementParty), + expensesPaid: many(expense, { relationName: 'ExpensePayer' }), + expensesCreated: many(expense, { relationName: 'ExpenseCreator' }), + expenseShares: many(expenseShare), + settlementsPaid: many(settlement, { relationName: 'SettlementFrom' }), + settlementsRecvd: many(settlement, { relationName: 'SettlementTo' }), + learningRecords: many(learningRecord), + careAssignments: many(careAssignment), + appointments: many(appointment), + careAttributes: many(careAttribute), + opportunityApplications: many(opportunityApplication), + marketplacePostsCreated: many(marketplacePost, { relationName: 'MarketplacePostedBy' }), + marketplacePostsClaimed: many(marketplacePost, { relationName: 'MarketplacePostClaimedBy' }), + houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByResident' }), + eventRsvps: many(eventRsvp), })) export const residentPhotoRelations = relations(residentPhoto, ({ one }) => ({ - resident: one(resident, { - fields: [residentPhoto.residentId], - references: [resident.id], - }), + resident: one(resident, { + fields: [residentPhoto.residentId], + references: [resident.id], + }), })) export const residentDocumentRelations = relations(residentDocument, ({ one }) => ({ - resident: one(resident, { - fields: [residentDocument.residentId], - references: [resident.id], - }), - uploadedBy: one(user, { - fields: [residentDocument.uploadedByUserId], - references: [user.id], - relationName: 'DocumentUploadedBy', - }), - blob: one(residentDocumentBlob), + resident: one(resident, { + fields: [residentDocument.residentId], + references: [resident.id], + }), + uploadedBy: one(user, { + fields: [residentDocument.uploadedByUserId], + references: [user.id], + relationName: 'DocumentUploadedBy', + }), + blob: one(residentDocumentBlob), })) export const residentDocumentBlobRelations = relations(residentDocumentBlob, ({ one }) => ({ - document: one(residentDocument, { - fields: [residentDocumentBlob.documentId], - references: [residentDocument.id], - }), + document: one(residentDocument, { + fields: [residentDocumentBlob.documentId], + references: [residentDocument.id], + }), })) export const complaintRelations = relations(complaint, ({ one }) => ({ - resident: one(resident, { - fields: [complaint.residentId], - references: [resident.id], - }), - respondedBy: one(user, { - fields: [complaint.respondedByUserId], - references: [user.id], - relationName: 'ComplaintRespondedBy', - }), + resident: one(resident, { + fields: [complaint.residentId], + references: [resident.id], + }), + respondedBy: one(user, { + fields: [complaint.respondedByUserId], + references: [user.id], + relationName: 'ComplaintRespondedBy', + }), })) export const messageThreadRelations = relations(messageThread, ({ one, many }) => ({ - resident: one(resident, { - fields: [messageThread.residentId], - references: [resident.id], - }), - messages: many(message), + resident: one(resident, { + fields: [messageThread.residentId], + references: [resident.id], + }), + messages: many(message), })) export const messageRelations = relations(message, ({ one }) => ({ - thread: one(messageThread, { - fields: [message.threadId], - references: [messageThread.id], - }), - authorResident: one(resident, { - fields: [message.authorResidentId], - references: [resident.id], - relationName: 'MessageAuthor', - }), - authorUser: one(user, { - fields: [message.authorUserId], - references: [user.id], - relationName: 'MessageAuthor', - }), + thread: one(messageThread, { + fields: [message.threadId], + references: [messageThread.id], + }), + authorResident: one(resident, { + fields: [message.authorResidentId], + references: [resident.id], + relationName: 'MessageAuthor', + }), + authorUser: one(user, { + fields: [message.authorUserId], + references: [user.id], + relationName: 'MessageAuthor', + }), })) export const housingUnitRelations = relations(housingUnit, ({ many }) => ({ - spots: many(placementSpot), - placements: many(placement), - incidents: many(incident), - maintenanceRequests: many(maintenanceRequest), - householdTasks: many(householdTask), - transferRequests: many(transferRequest), - marketplacePosts: many(marketplacePost), - houseEvents: many(houseEvent), - houseRules: many(houseRule), - proposals: many(proposal), - expenses: many(expense), - settlements: many(settlement), + spots: many(placementSpot), + placements: many(placement), + incidents: many(incident), + maintenanceRequests: many(maintenanceRequest), + householdTasks: many(householdTask), + transferRequests: many(transferRequest), + marketplacePosts: many(marketplacePost), + houseEvents: many(houseEvent), + houseRules: many(houseRule), + proposals: many(proposal), + expenses: many(expense), + settlements: many(settlement), + staffAccess: many(staffUnit), })) export const expenseRelations = relations(expense, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [expense.housingUnitId], - references: [housingUnit.id], - }), - paidBy: one(resident, { - fields: [expense.paidById], - references: [resident.id], - relationName: 'ExpensePayer', - }), - createdBy: one(resident, { - fields: [expense.createdById], - references: [resident.id], - relationName: 'ExpenseCreator', - }), - shares: many(expenseShare), + housingUnit: one(housingUnit, { + fields: [expense.housingUnitId], + references: [housingUnit.id], + }), + paidBy: one(resident, { + fields: [expense.paidById], + references: [resident.id], + relationName: 'ExpensePayer', + }), + createdBy: one(resident, { + fields: [expense.createdById], + references: [resident.id], + relationName: 'ExpenseCreator', + }), + shares: many(expenseShare), })) export const expenseShareRelations = relations(expenseShare, ({ one }) => ({ - expense: one(expense, { - fields: [expenseShare.expenseId], - references: [expense.id], - }), - resident: one(resident, { - fields: [expenseShare.residentId], - references: [resident.id], - }), + expense: one(expense, { + fields: [expenseShare.expenseId], + references: [expense.id], + }), + resident: one(resident, { + fields: [expenseShare.residentId], + references: [resident.id], + }), })) export const settlementRelations = relations(settlement, ({ one }) => ({ - housingUnit: one(housingUnit, { - fields: [settlement.housingUnitId], - references: [housingUnit.id], - }), - from: one(resident, { - fields: [settlement.fromId], - references: [resident.id], - relationName: 'SettlementFrom', - }), - to: one(resident, { - fields: [settlement.toId], - references: [resident.id], - relationName: 'SettlementTo', - }), + housingUnit: one(housingUnit, { + fields: [settlement.housingUnitId], + references: [housingUnit.id], + }), + from: one(resident, { + fields: [settlement.fromId], + references: [resident.id], + relationName: 'SettlementFrom', + }), + to: one(resident, { + fields: [settlement.toId], + references: [resident.id], + relationName: 'SettlementTo', + }), })) export const placementSpotRelations = relations(placementSpot, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [placementSpot.housingUnitId], - references: [housingUnit.id], - }), - parentSpot: one(placementSpot, { - fields: [placementSpot.parentSpotId], - references: [placementSpot.id], - relationName: 'SpotHierarchy', - }), - childSpots: many(placementSpot, { relationName: 'SpotHierarchy' }), - placements: many(placement), - maintenanceRequests: many(maintenanceRequest), + housingUnit: one(housingUnit, { + fields: [placementSpot.housingUnitId], + references: [housingUnit.id], + }), + parentSpot: one(placementSpot, { + fields: [placementSpot.parentSpotId], + references: [placementSpot.id], + relationName: 'SpotHierarchy', + }), + childSpots: many(placementSpot, { relationName: 'SpotHierarchy' }), + placements: many(placement), + maintenanceRequests: many(maintenanceRequest), })) export const placementRelations = relations(placement, ({ one, many }) => ({ - resident: one(resident, { - fields: [placement.residentId], - references: [resident.id], - }), - housingUnit: one(housingUnit, { - fields: [placement.housingUnitId], - references: [housingUnit.id], - }), - spot: one(placementSpot, { - fields: [placement.spotId], - references: [placementSpot.id], - }), - relatedIncident: one(incident, { - fields: [placement.relatedIncidentId], - references: [incident.id], - relationName: 'PlacementConflictIncident', - }), - incidents: many(incident, { relationName: 'IncidentPlacement' }), - checkIns: many(satisfactionCheckIn), - transferRequests: many(transferRequest, { relationName: 'TransferFromPlacement' }), + resident: one(resident, { + fields: [placement.residentId], + references: [resident.id], + }), + housingUnit: one(housingUnit, { + fields: [placement.housingUnitId], + references: [housingUnit.id], + }), + spot: one(placementSpot, { + fields: [placement.spotId], + references: [placementSpot.id], + }), + relatedIncident: one(incident, { + fields: [placement.relatedIncidentId], + references: [incident.id], + relationName: 'PlacementConflictIncident', + }), + incidents: many(incident, { relationName: 'IncidentPlacement' }), + checkIns: many(satisfactionCheckIn), + transferRequests: many(transferRequest, { relationName: 'TransferFromPlacement' }), })) export const compatibilityAssessmentRelations = relations(compatibilityAssessment, ({ one }) => ({ - resident: one(resident, { - fields: [compatibilityAssessment.residentId], - references: [resident.id], - relationName: 'ResidentAssessments', - }), - comparedWith: one(resident, { - fields: [compatibilityAssessment.comparedWithId], - references: [resident.id], - relationName: 'ComparedResidentAssessments', - }), + resident: one(resident, { + fields: [compatibilityAssessment.residentId], + references: [resident.id], + relationName: 'ResidentAssessments', + }), + comparedWith: one(resident, { + fields: [compatibilityAssessment.comparedWithId], + references: [resident.id], + relationName: 'ComparedResidentAssessments', + }), })) export const incidentRelations = relations(incident, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [incident.housingUnitId], - references: [housingUnit.id], - }), - placement: one(placement, { - fields: [incident.placementId], - references: [placement.id], - relationName: 'IncidentPlacement', - }), - reportedBy: one(resident, { - fields: [incident.reportedById], - references: [resident.id], - relationName: 'IncidentReporter', - }), - subject: one(resident, { - fields: [incident.subjectId], - references: [resident.id], - relationName: 'IncidentSubject', - }), - involvedResidents: many(incidentInvolvement), - followUps: many(incidentFollowUp), - agreements: many(conflictAgreement), - conflictPlacements: many(placement, { relationName: 'PlacementConflictIncident' }), + housingUnit: one(housingUnit, { + fields: [incident.housingUnitId], + references: [housingUnit.id], + }), + placement: one(placement, { + fields: [incident.placementId], + references: [placement.id], + relationName: 'IncidentPlacement', + }), + reportedBy: one(resident, { + fields: [incident.reportedById], + references: [resident.id], + relationName: 'IncidentReporter', + }), + subject: one(resident, { + fields: [incident.subjectId], + references: [resident.id], + relationName: 'IncidentSubject', + }), + involvedResidents: many(incidentInvolvement), + followUps: many(incidentFollowUp), + agreements: many(conflictAgreement), + conflictPlacements: many(placement, { relationName: 'PlacementConflictIncident' }), })) export const incidentFollowUpRelations = relations(incidentFollowUp, ({ one }) => ({ - incident: one(incident, { - fields: [incidentFollowUp.incidentId], - references: [incident.id], - }), + incident: one(incident, { + fields: [incidentFollowUp.incidentId], + references: [incident.id], + }), })) export const incidentInvolvementRelations = relations(incidentInvolvement, ({ one }) => ({ - incident: one(incident, { - fields: [incidentInvolvement.incidentId], - references: [incident.id], - }), - resident: one(resident, { - fields: [incidentInvolvement.residentId], - references: [resident.id], - }), + incident: one(incident, { + fields: [incidentInvolvement.incidentId], + references: [incident.id], + }), + resident: one(resident, { + fields: [incidentInvolvement.residentId], + references: [resident.id], + }), })) export const satisfactionCheckInRelations = relations(satisfactionCheckIn, ({ one }) => ({ - placement: one(placement, { - fields: [satisfactionCheckIn.placementId], - references: [placement.id], - }), - collectedByUser: one(user, { - fields: [satisfactionCheckIn.collectedByUserId], - references: [user.id], - relationName: 'CheckInCollectedBy', - }), - appointment: one(appointment, { - fields: [satisfactionCheckIn.appointmentId], - references: [appointment.id], - }), + placement: one(placement, { + fields: [satisfactionCheckIn.placementId], + references: [placement.id], + }), + collectedByUser: one(user, { + fields: [satisfactionCheckIn.collectedByUserId], + references: [user.id], + relationName: 'CheckInCollectedBy', + }), + appointment: one(appointment, { + fields: [satisfactionCheckIn.appointmentId], + references: [appointment.id], + }), })) export const userRelations = relations(user, ({ one, many }) => ({ - messagesWritten: many(message, { relationName: 'MessageAuthor' }), - auditLogs: many(auditLog), - activitiesCreated: many(activity, { relationName: 'ActivityCreatedBy' }), - activitiesUpdated: many(activity, { relationName: 'ActivityUpdatedBy' }), - careAssignments: many(careAssignment), - appointments: many(appointment), - careAttributesUpdated: many(careAttribute), - houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByStaff' }), - opportunitiesCreated: many(opportunity, { relationName: 'OpportunityCreatedBy' }), - opportunitiesUpdated: many(opportunity, { relationName: 'OpportunityUpdatedBy' }), - applicationsSupported: many(opportunityApplication, { relationName: 'ApplicationSupportedBy' }), - checkInsCollected: many(satisfactionCheckIn, { relationName: 'CheckInCollectedBy' }), - documentsUploaded: many(residentDocument, { relationName: 'DocumentUploadedBy' }), - complaintsAnswered: many(complaint, { relationName: 'ComplaintRespondedBy' }), - account: one(account), + messagesWritten: many(message, { relationName: 'MessageAuthor' }), + auditLogs: many(auditLog), + activitiesCreated: many(activity, { relationName: 'ActivityCreatedBy' }), + activitiesUpdated: many(activity, { relationName: 'ActivityUpdatedBy' }), + careAssignments: many(careAssignment), + appointments: many(appointment), + careAttributesUpdated: many(careAttribute), + houseEventsCreated: many(houseEvent, { relationName: 'HouseEventCreatedByStaff' }), + opportunitiesCreated: many(opportunity, { relationName: 'OpportunityCreatedBy' }), + opportunitiesUpdated: many(opportunity, { relationName: 'OpportunityUpdatedBy' }), + applicationsSupported: many(opportunityApplication, { relationName: 'ApplicationSupportedBy' }), + checkInsCollected: many(satisfactionCheckIn, { relationName: 'CheckInCollectedBy' }), + documentsUploaded: many(residentDocument, { relationName: 'DocumentUploadedBy' }), + complaintsAnswered: many(complaint, { relationName: 'ComplaintRespondedBy' }), + account: one(account), + unitAccess: many(staffUnit), })) export const accountRelations = relations(account, ({ one, many }) => ({ - user: one(user, { - fields: [account.userId], - references: [user.id], - }), - resident: one(resident, { - fields: [account.residentId], - references: [resident.id], - }), - authTokens: many(authToken), + user: one(user, { + fields: [account.userId], + references: [user.id], + }), + resident: one(resident, { + fields: [account.residentId], + references: [resident.id], + }), + authTokens: many(authToken), })) export const authTokenRelations = relations(authToken, ({ one }) => ({ - account: one(account, { - fields: [authToken.accountId], - references: [account.id], - }), + account: one(account, { + fields: [authToken.accountId], + references: [account.id], + }), })) export const auditLogRelations = relations(auditLog, ({ one }) => ({ - user: one(user, { - fields: [auditLog.userId], - references: [user.id], - }), + user: one(user, { + fields: [auditLog.userId], + references: [user.id], + }), })) export const activityRelations = relations(activity, ({ one }) => ({ - createdBy: one(user, { - fields: [activity.createdByUserId], - references: [user.id], - relationName: 'ActivityCreatedBy', - }), - updatedBy: one(user, { - fields: [activity.updatedByUserId], - references: [user.id], - relationName: 'ActivityUpdatedBy', - }), + createdBy: one(user, { + fields: [activity.createdByUserId], + references: [user.id], + relationName: 'ActivityCreatedBy', + }), + updatedBy: one(user, { + fields: [activity.updatedByUserId], + references: [user.id], + relationName: 'ActivityUpdatedBy', + }), })) export const houseEventRelations = relations(houseEvent, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [houseEvent.housingUnitId], - references: [housingUnit.id], - }), - createdByStaff: one(user, { - fields: [houseEvent.createdByStaffId], - references: [user.id], - relationName: 'HouseEventCreatedByStaff', - }), - createdByResident: one(resident, { - fields: [houseEvent.createdByResidentId], - references: [resident.id], - relationName: 'HouseEventCreatedByResident', - }), - rsvps: many(eventRsvp), + housingUnit: one(housingUnit, { + fields: [houseEvent.housingUnitId], + references: [housingUnit.id], + }), + createdByStaff: one(user, { + fields: [houseEvent.createdByStaffId], + references: [user.id], + relationName: 'HouseEventCreatedByStaff', + }), + createdByResident: one(resident, { + fields: [houseEvent.createdByResidentId], + references: [resident.id], + relationName: 'HouseEventCreatedByResident', + }), + rsvps: many(eventRsvp), })) export const eventRsvpRelations = relations(eventRsvp, ({ one }) => ({ - event: one(houseEvent, { - fields: [eventRsvp.eventId], - references: [houseEvent.id], - }), - resident: one(resident, { - fields: [eventRsvp.residentId], - references: [resident.id], - }), + event: one(houseEvent, { + fields: [eventRsvp.eventId], + references: [houseEvent.id], + }), + resident: one(resident, { + fields: [eventRsvp.residentId], + references: [resident.id], + }), })) export const maintenanceRequestRelations = relations(maintenanceRequest, ({ one }) => ({ - housingUnit: one(housingUnit, { - fields: [maintenanceRequest.housingUnitId], - references: [housingUnit.id], - }), - spot: one(placementSpot, { - fields: [maintenanceRequest.spotId], - references: [placementSpot.id], - }), - reportedBy: one(resident, { - fields: [maintenanceRequest.reportedById], - references: [resident.id], - }), + housingUnit: one(housingUnit, { + fields: [maintenanceRequest.housingUnitId], + references: [housingUnit.id], + }), + spot: one(placementSpot, { + fields: [maintenanceRequest.spotId], + references: [placementSpot.id], + }), + reportedBy: one(resident, { + fields: [maintenanceRequest.reportedById], + references: [resident.id], + }), })) export const householdTaskRelations = relations(householdTask, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [householdTask.housingUnitId], - references: [housingUnit.id], - }), - createdByResident: one(resident, { - fields: [householdTask.createdByResidentId], - references: [resident.id], - relationName: 'TaskCreator', - }), - completions: many(taskCompletion), - attentionFlags: many(taskAttentionFlag), - requests: many(taskRequest), + housingUnit: one(housingUnit, { + fields: [householdTask.housingUnitId], + references: [housingUnit.id], + }), + createdByResident: one(resident, { + fields: [householdTask.createdByResidentId], + references: [resident.id], + relationName: 'TaskCreator', + }), + completions: many(taskCompletion), + attentionFlags: many(taskAttentionFlag), + requests: many(taskRequest), })) export const taskCompletionRelations = relations(taskCompletion, ({ one, many }) => ({ - task: one(householdTask, { - fields: [taskCompletion.taskId], - references: [householdTask.id], - }), - completedBy: one(resident, { - fields: [taskCompletion.completedById], - references: [resident.id], - }), - resolvedFlags: many(taskAttentionFlag, { relationName: 'FlagResolvedByCompletion' }), - fulfilledRequests: many(taskRequest, { relationName: 'RequestFulfilledByCompletion' }), + task: one(householdTask, { + fields: [taskCompletion.taskId], + references: [householdTask.id], + }), + completedBy: one(resident, { + fields: [taskCompletion.completedById], + references: [resident.id], + }), + resolvedFlags: many(taskAttentionFlag, { relationName: 'FlagResolvedByCompletion' }), + fulfilledRequests: many(taskRequest, { relationName: 'RequestFulfilledByCompletion' }), })) export const taskAttentionFlagRelations = relations(taskAttentionFlag, ({ one }) => ({ - task: one(householdTask, { - fields: [taskAttentionFlag.taskId], - references: [householdTask.id], - }), - flaggedBy: one(resident, { - fields: [taskAttentionFlag.flaggedById], - references: [resident.id], - }), - resolvedByCompletion: one(taskCompletion, { - fields: [taskAttentionFlag.resolvedByCompletionId], - references: [taskCompletion.id], - relationName: 'FlagResolvedByCompletion', - }), + task: one(householdTask, { + fields: [taskAttentionFlag.taskId], + references: [householdTask.id], + }), + flaggedBy: one(resident, { + fields: [taskAttentionFlag.flaggedById], + references: [resident.id], + }), + resolvedByCompletion: one(taskCompletion, { + fields: [taskAttentionFlag.resolvedByCompletionId], + references: [taskCompletion.id], + relationName: 'FlagResolvedByCompletion', + }), })) export const taskRequestRelations = relations(taskRequest, ({ one }) => ({ - task: one(householdTask, { - fields: [taskRequest.taskId], - references: [householdTask.id], - }), - requestedBy: one(resident, { - fields: [taskRequest.requestedById], - references: [resident.id], - relationName: 'TaskRequestsMade', - }), - requestedResident: one(resident, { - fields: [taskRequest.requestedResidentId], - references: [resident.id], - relationName: 'TaskRequestsReceived', - }), - completion: one(taskCompletion, { - fields: [taskRequest.completionId], - references: [taskCompletion.id], - relationName: 'RequestFulfilledByCompletion', - }), + task: one(householdTask, { + fields: [taskRequest.taskId], + references: [householdTask.id], + }), + requestedBy: one(resident, { + fields: [taskRequest.requestedById], + references: [resident.id], + relationName: 'TaskRequestsMade', + }), + requestedResident: one(resident, { + fields: [taskRequest.requestedResidentId], + references: [resident.id], + relationName: 'TaskRequestsReceived', + }), + completion: one(taskCompletion, { + fields: [taskRequest.completionId], + references: [taskCompletion.id], + relationName: 'RequestFulfilledByCompletion', + }), })) export const marketplacePostRelations = relations(marketplacePost, ({ one }) => ({ - housingUnit: one(housingUnit, { - fields: [marketplacePost.housingUnitId], - references: [housingUnit.id], - }), - postedBy: one(resident, { - fields: [marketplacePost.postedById], - references: [resident.id], - relationName: 'MarketplacePostedBy', - }), - claimedBy: one(resident, { - fields: [marketplacePost.claimedById], - references: [resident.id], - relationName: 'MarketplacePostClaimedBy', - }), + housingUnit: one(housingUnit, { + fields: [marketplacePost.housingUnitId], + references: [housingUnit.id], + }), + postedBy: one(resident, { + fields: [marketplacePost.postedById], + references: [resident.id], + relationName: 'MarketplacePostedBy', + }), + claimedBy: one(resident, { + fields: [marketplacePost.claimedById], + references: [resident.id], + relationName: 'MarketplacePostClaimedBy', + }), })) export const transferRequestRelations = relations(transferRequest, ({ one }) => ({ - resident: one(resident, { - fields: [transferRequest.residentId], - references: [resident.id], - }), - currentPlacement: one(placement, { - fields: [transferRequest.currentPlacementId], - references: [placement.id], - relationName: 'TransferFromPlacement', - }), - targetUnit: one(housingUnit, { - fields: [transferRequest.targetUnitId], - references: [housingUnit.id], - }), + resident: one(resident, { + fields: [transferRequest.residentId], + references: [resident.id], + }), + currentPlacement: one(placement, { + fields: [transferRequest.currentPlacementId], + references: [placement.id], + relationName: 'TransferFromPlacement', + }), + targetUnit: one(housingUnit, { + fields: [transferRequest.targetUnitId], + references: [housingUnit.id], + }), })) export const houseRuleRelations = relations(houseRule, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [houseRule.housingUnitId], - references: [housingUnit.id], - }), - parentRule: one(houseRule, { - fields: [houseRule.parentRuleId], - references: [houseRule.id], - relationName: 'RuleSpecialisation', - }), - childRules: many(houseRule, { relationName: 'RuleSpecialisation' }), - adoptedByProposal: one(proposal, { - fields: [houseRule.adoptedByProposalId], - references: [proposal.id], - relationName: 'ProposalAdoptedRule', - }), - acknowledgements: many(ruleAcknowledgement), - targetedBy: many(proposal, { relationName: 'ProposalTargetRule' }), - topicProposals: many(proposal, { relationName: 'ProposalTopicRule' }), + housingUnit: one(housingUnit, { + fields: [houseRule.housingUnitId], + references: [housingUnit.id], + }), + parentRule: one(houseRule, { + fields: [houseRule.parentRuleId], + references: [houseRule.id], + relationName: 'RuleSpecialisation', + }), + childRules: many(houseRule, { relationName: 'RuleSpecialisation' }), + adoptedByProposal: one(proposal, { + fields: [houseRule.adoptedByProposalId], + references: [proposal.id], + relationName: 'ProposalAdoptedRule', + }), + acknowledgements: many(ruleAcknowledgement), + targetedBy: many(proposal, { relationName: 'ProposalTargetRule' }), + topicProposals: many(proposal, { relationName: 'ProposalTopicRule' }), })) export const ruleAcknowledgementRelations = relations(ruleAcknowledgement, ({ one }) => ({ - rule: one(houseRule, { - fields: [ruleAcknowledgement.ruleId], - references: [houseRule.id], - }), - resident: one(resident, { - fields: [ruleAcknowledgement.residentId], - references: [resident.id], - }), + rule: one(houseRule, { + fields: [ruleAcknowledgement.ruleId], + references: [houseRule.id], + }), + resident: one(resident, { + fields: [ruleAcknowledgement.residentId], + references: [resident.id], + }), })) export const proposalRelations = relations(proposal, ({ one, many }) => ({ - housingUnit: one(housingUnit, { - fields: [proposal.housingUnitId], - references: [housingUnit.id], - }), - targetRule: one(houseRule, { - fields: [proposal.targetRuleId], - references: [houseRule.id], - relationName: 'ProposalTargetRule', - }), - parentOrgRule: one(houseRule, { - fields: [proposal.parentOrgRuleId], - references: [houseRule.id], - relationName: 'ProposalTopicRule', - }), - proposedByResident: one(resident, { - fields: [proposal.proposedByResidentId], - references: [resident.id], - relationName: 'ProposalAuthor', - }), - votes: many(vote), - adoptedRules: many(houseRule, { relationName: 'ProposalAdoptedRule' }), - agreement: one(conflictAgreement), + housingUnit: one(housingUnit, { + fields: [proposal.housingUnitId], + references: [housingUnit.id], + }), + targetRule: one(houseRule, { + fields: [proposal.targetRuleId], + references: [houseRule.id], + relationName: 'ProposalTargetRule', + }), + parentOrgRule: one(houseRule, { + fields: [proposal.parentOrgRuleId], + references: [houseRule.id], + relationName: 'ProposalTopicRule', + }), + proposedByResident: one(resident, { + fields: [proposal.proposedByResidentId], + references: [resident.id], + relationName: 'ProposalAuthor', + }), + votes: many(vote), + adoptedRules: many(houseRule, { relationName: 'ProposalAdoptedRule' }), + agreement: one(conflictAgreement), })) export const voteRelations = relations(vote, ({ one }) => ({ - proposal: one(proposal, { - fields: [vote.proposalId], - references: [proposal.id], - }), - resident: one(resident, { - fields: [vote.residentId], - references: [resident.id], - }), + proposal: one(proposal, { + fields: [vote.proposalId], + references: [proposal.id], + }), + resident: one(resident, { + fields: [vote.residentId], + references: [resident.id], + }), })) export const conflictAgreementRelations = relations(conflictAgreement, ({ one, many }) => ({ - incident: one(incident, { - fields: [conflictAgreement.incidentId], - references: [incident.id], - }), - parties: many(agreementParty), - ruleProposal: one(proposal, { - fields: [conflictAgreement.ruleProposalId], - references: [proposal.id], - }), + incident: one(incident, { + fields: [conflictAgreement.incidentId], + references: [incident.id], + }), + parties: many(agreementParty), + ruleProposal: one(proposal, { + fields: [conflictAgreement.ruleProposalId], + references: [proposal.id], + }), })) export const agreementPartyRelations = relations(agreementParty, ({ one }) => ({ - agreement: one(conflictAgreement, { - fields: [agreementParty.agreementId], - references: [conflictAgreement.id], - }), - resident: one(resident, { - fields: [agreementParty.residentId], - references: [resident.id], - }), + agreement: one(conflictAgreement, { + fields: [agreementParty.agreementId], + references: [conflictAgreement.id], + }), + resident: one(resident, { + fields: [agreementParty.residentId], + references: [resident.id], + }), })) export const learningRecordRelations = relations(learningRecord, ({ one }) => ({ - resident: one(resident, { - fields: [learningRecord.residentId], - references: [resident.id], - }), - fromApplication: one(opportunityApplication), + resident: one(resident, { + fields: [learningRecord.residentId], + references: [resident.id], + }), + fromApplication: one(opportunityApplication), })) export const careAssignmentRelations = relations(careAssignment, ({ one }) => ({ - resident: one(resident, { - fields: [careAssignment.residentId], - references: [resident.id], - }), - staff: one(user, { - fields: [careAssignment.staffId], - references: [user.id], - }), + resident: one(resident, { + fields: [careAssignment.residentId], + references: [resident.id], + }), + staff: one(user, { + fields: [careAssignment.staffId], + references: [user.id], + }), })) export const appointmentRelations = relations(appointment, ({ one }) => ({ - resident: one(resident, { - fields: [appointment.residentId], - references: [resident.id], - }), - staff: one(user, { - fields: [appointment.staffId], - references: [user.id], - }), - checkIn: one(satisfactionCheckIn), + resident: one(resident, { + fields: [appointment.residentId], + references: [resident.id], + }), + staff: one(user, { + fields: [appointment.staffId], + references: [user.id], + }), + checkIn: one(satisfactionCheckIn), })) export const careAttributeRelations = relations(careAttribute, ({ one }) => ({ - resident: one(resident, { - fields: [careAttribute.residentId], - references: [resident.id], - }), - updatedBy: one(user, { - fields: [careAttribute.updatedById], - references: [user.id], - }), + resident: one(resident, { + fields: [careAttribute.residentId], + references: [resident.id], + }), + updatedBy: one(user, { + fields: [careAttribute.updatedById], + references: [user.id], + }), })) export const opportunityRelations = relations(opportunity, ({ one, many }) => ({ - createdBy: one(user, { - fields: [opportunity.createdByUserId], - references: [user.id], - relationName: 'OpportunityCreatedBy', - }), - updatedBy: one(user, { - fields: [opportunity.updatedByUserId], - references: [user.id], - relationName: 'OpportunityUpdatedBy', - }), - applications: many(opportunityApplication), + createdBy: one(user, { + fields: [opportunity.createdByUserId], + references: [user.id], + relationName: 'OpportunityCreatedBy', + }), + updatedBy: one(user, { + fields: [opportunity.updatedByUserId], + references: [user.id], + relationName: 'OpportunityUpdatedBy', + }), + applications: many(opportunityApplication), })) export const opportunityApplicationRelations = relations(opportunityApplication, ({ one }) => ({ - resident: one(resident, { - fields: [opportunityApplication.residentId], - references: [resident.id], - }), - opportunity: one(opportunity, { - fields: [opportunityApplication.opportunityId], - references: [opportunity.id], - }), - supportedBy: one(user, { - fields: [opportunityApplication.supportedByUserId], - references: [user.id], - relationName: 'ApplicationSupportedBy', - }), - learningRecord: one(learningRecord, { - fields: [opportunityApplication.learningRecordId], - references: [learningRecord.id], - }), + resident: one(resident, { + fields: [opportunityApplication.residentId], + references: [resident.id], + }), + opportunity: one(opportunity, { + fields: [opportunityApplication.opportunityId], + references: [opportunity.id], + }), + supportedBy: one(user, { + fields: [opportunityApplication.supportedByUserId], + references: [user.id], + relationName: 'ApplicationSupportedBy', + }), + learningRecord: one(learningRecord, { + fields: [opportunityApplication.learningRecordId], + references: [learningRecord.id], + }), +})) + +export const staffUnitRelations = relations(staffUnit, ({ one }) => ({ + staff: one(user, { + fields: [staffUnit.staffId], + references: [user.id], + }), + housingUnit: one(housingUnit, { + fields: [staffUnit.housingUnitId], + references: [housingUnit.id], + }), })) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index cb9f0458..e60f5f89 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -302,6 +302,7 @@ export const ruleDelegation = pgEnum('RuleDelegation', [ ]) export const ruleScope = pgEnum('RuleScope', ['ORG', 'UNIT']) export const ruleStatus = pgEnum('RuleStatus', ['ACTIVE', 'SUPERSEDED', 'ARCHIVED']) +export const siteAccess = pgEnum('SiteAccess', ['ALL_UNITS', 'ASSIGNED_UNITS']) export const sleepSchedule = pgEnum('SleepSchedule', [ 'EARLY_BIRD', 'STANDARD', @@ -932,6 +933,7 @@ export const user = pgTable( code: text().notNull(), scope: staffScope().default('OWN_DOMAIN').notNull(), isSystemAdmin: boolean().default(false).notNull(), + siteAccess: siteAccess().default('ALL_UNITS').notNull(), }, (table) => [ index('User_code_idx').using('btree', table.code.asc().nullsLast()), @@ -2433,3 +2435,44 @@ export const residentDocumentBlob = pgTable( .onDelete('cascade'), ], ) + +/** + * Which PLACES a staff member is responsible for — the join behind + * `User.siteAccess = 'ASSIGNED_UNITS'`. A join, not a String[] of unit ids on + * User: a unit that closes must take its access rows with it. Cascade both + * ways — pure access wiring, no history worth keeping once either side is + * gone. + */ +export const staffUnit = pgTable( + 'StaffUnit', + { + id: text().primaryKey().$defaultFn(createId).notNull(), + createdAt: timestamp({ precision: 3, mode: 'date' }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + staffId: text().notNull(), + housingUnitId: text().notNull(), + }, + (table) => [ + uniqueIndex('StaffUnit_staffId_housingUnitId_key').using( + 'btree', + table.staffId.asc().nullsLast(), + table.housingUnitId.asc().nullsLast(), + ), + index('StaffUnit_housingUnitId_idx').using('btree', table.housingUnitId.asc().nullsLast()), + foreignKey({ + columns: [table.staffId], + foreignColumns: [user.id], + name: 'StaffUnit_staffId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + foreignKey({ + columns: [table.housingUnitId], + foreignColumns: [housingUnit.id], + name: 'StaffUnit_housingUnitId_fkey', + }) + .onUpdate('cascade') + .onDelete('cascade'), + ], +) diff --git a/src/lib/db/types.ts b/src/lib/db/types.ts index f66e9b08..2626989d 100644 --- a/src/lib/db/types.ts +++ b/src/lib/db/types.ts @@ -25,6 +25,7 @@ export type HousingUnit = typeof s.housingUnit.$inferSelect export type HouseholdTask = typeof s.householdTask.$inferSelect export type SatisfactionCheckIn = typeof s.satisfactionCheckIn.$inferSelect export type User = typeof s.user.$inferSelect +export type StaffUnit = typeof s.staffUnit.$inferSelect export type TaskRequest = typeof s.taskRequest.$inferSelect export type TransferRequest = typeof s.transferRequest.$inferSelect export type Resident = typeof s.resident.$inferSelect @@ -75,6 +76,7 @@ export type NewHousingUnit = typeof s.housingUnit.$inferInsert export type NewHouseholdTask = typeof s.householdTask.$inferInsert export type NewSatisfactionCheckIn = typeof s.satisfactionCheckIn.$inferInsert export type NewUser = typeof s.user.$inferInsert +export type NewStaffUnit = typeof s.staffUnit.$inferInsert export type NewTaskRequest = typeof s.taskRequest.$inferInsert export type NewTransferRequest = typeof s.transferRequest.$inferInsert export type NewResident = typeof s.resident.$inferInsert @@ -113,571 +115,579 @@ export type NewResidentDocumentBlob = typeof s.residentDocumentBlob.$inferInsert // Enums: value objects + union types, Prisma-style // --------------------------------------------------------------------------- export const ActivityCategory = Object.freeze({ - SPORT: 'SPORT', - LANGUAGE: 'LANGUAGE', - CULTURE: 'CULTURE', - COMMUNITY: 'COMMUNITY', - FAMILY: 'FAMILY', - SUPPORT: 'SUPPORT', + SPORT: 'SPORT', + LANGUAGE: 'LANGUAGE', + CULTURE: 'CULTURE', + COMMUNITY: 'COMMUNITY', + FAMILY: 'FAMILY', + SUPPORT: 'SUPPORT', } as const) satisfies Record export type ActivityCategory = (typeof ActivityCategory)[keyof typeof ActivityCategory] export const ActivityCost = Object.freeze({ - FREE: 'FREE', - REDUCED: 'REDUCED', - PAID: 'PAID', + FREE: 'FREE', + REDUCED: 'REDUCED', + PAID: 'PAID', } as const) satisfies Record export type ActivityCost = (typeof ActivityCost)[keyof typeof ActivityCost] export const ActivityStatus = Object.freeze({ - DRAFT: 'DRAFT', - PUBLISHED: 'PUBLISHED', - ARCHIVED: 'ARCHIVED', + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + ARCHIVED: 'ARCHIVED', } as const) satisfies Record export type ActivityStatus = (typeof ActivityStatus)[keyof typeof ActivityStatus] export const AgeRange = Object.freeze({ - YOUNG_ADULT: 'YOUNG_ADULT', - ADULT: 'ADULT', - MIDDLE_AGED: 'MIDDLE_AGED', - SENIOR: 'SENIOR', + YOUNG_ADULT: 'YOUNG_ADULT', + ADULT: 'ADULT', + MIDDLE_AGED: 'MIDDLE_AGED', + SENIOR: 'SENIOR', } as const) satisfies Record export type AgeRange = (typeof AgeRange)[keyof typeof AgeRange] export const AgreementStatus = Object.freeze({ - PROPOSED: 'PROPOSED', - ACCEPTED: 'ACCEPTED', - HELD: 'HELD', - BROKEN: 'BROKEN', - EXPIRED: 'EXPIRED', + PROPOSED: 'PROPOSED', + ACCEPTED: 'ACCEPTED', + HELD: 'HELD', + BROKEN: 'BROKEN', + EXPIRED: 'EXPIRED', } as const) satisfies Record export type AgreementStatus = (typeof AgreementStatus)[keyof typeof AgreementStatus] export const ApplicationStage = Object.freeze({ - INTERESTED: 'INTERESTED', - APPLIED: 'APPLIED', - INTERVIEW: 'INTERVIEW', - ACCEPTED: 'ACCEPTED', - STARTED: 'STARTED', - ENDED: 'ENDED', - DECLINED: 'DECLINED', + INTERESTED: 'INTERESTED', + APPLIED: 'APPLIED', + INTERVIEW: 'INTERVIEW', + ACCEPTED: 'ACCEPTED', + STARTED: 'STARTED', + ENDED: 'ENDED', + DECLINED: 'DECLINED', } as const) satisfies Record export type ApplicationStage = (typeof ApplicationStage)[keyof typeof ApplicationStage] export const AppointmentStatus = Object.freeze({ - SCHEDULED: 'SCHEDULED', - COMPLETED: 'COMPLETED', - CANCELLED: 'CANCELLED', - NO_SHOW: 'NO_SHOW', - REQUESTED: 'REQUESTED', + SCHEDULED: 'SCHEDULED', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', + NO_SHOW: 'NO_SHOW', + REQUESTED: 'REQUESTED', } as const) satisfies Record export type AppointmentStatus = (typeof AppointmentStatus)[keyof typeof AppointmentStatus] export const AuthTokenPurpose = Object.freeze({ - VERIFY_EMAIL: 'VERIFY_EMAIL', - RESET_PASSWORD: 'RESET_PASSWORD', + VERIFY_EMAIL: 'VERIFY_EMAIL', + RESET_PASSWORD: 'RESET_PASSWORD', } as const) satisfies Record export type AuthTokenPurpose = (typeof AuthTokenPurpose)[keyof typeof AuthTokenPurpose] export const CareRole = Object.freeze({ - HOUSING: 'HOUSING', - SOCIAL: 'SOCIAL', - JOB: 'JOB', - VOLUNTEERING: 'VOLUNTEERING', + HOUSING: 'HOUSING', + SOCIAL: 'SOCIAL', + JOB: 'JOB', + VOLUNTEERING: 'VOLUNTEERING', } as const) satisfies Record export type CareRole = (typeof CareRole)[keyof typeof CareRole] export const CheckInType = Object.freeze({ - INITIAL: 'INITIAL', - REGULAR: 'REGULAR', - AD_HOC: 'AD_HOC', - EXIT: 'EXIT', + INITIAL: 'INITIAL', + REGULAR: 'REGULAR', + AD_HOC: 'AD_HOC', + EXIT: 'EXIT', } as const) satisfies Record export type CheckInType = (typeof CheckInType)[keyof typeof CheckInType] export const ComplaintStatus = Object.freeze({ - OPEN: 'OPEN', - IN_REVIEW: 'IN_REVIEW', - ANSWERED: 'ANSWERED', + OPEN: 'OPEN', + IN_REVIEW: 'IN_REVIEW', + ANSWERED: 'ANSWERED', } as const) satisfies Record export type ComplaintStatus = (typeof ComplaintStatus)[keyof typeof ComplaintStatus] export const ComplaintSubject = Object.freeze({ - STAFF: 'STAFF', - ACCOMMODATION: 'ACCOMMODATION', - DECISION: 'DECISION', - OTHER: 'OTHER', + STAFF: 'STAFF', + ACCOMMODATION: 'ACCOMMODATION', + DECISION: 'DECISION', + OTHER: 'OTHER', } as const) satisfies Record export type ComplaintSubject = (typeof ComplaintSubject)[keyof typeof ComplaintSubject] export const ConflictStyle = Object.freeze({ - AVOIDANT: 'AVOIDANT', - COOPERATIVE: 'COOPERATIVE', - DIRECT: 'DIRECT', + AVOIDANT: 'AVOIDANT', + COOPERATIVE: 'COOPERATIVE', + DIRECT: 'DIRECT', } as const) satisfies Record export type ConflictStyle = (typeof ConflictStyle)[keyof typeof ConflictStyle] export const DecisionMode = Object.freeze({ - RESIDENT_BINDING: 'RESIDENT_BINDING', - RESIDENT_ADVISORY: 'RESIDENT_ADVISORY', - STAFF_ONLY: 'STAFF_ONLY', + RESIDENT_BINDING: 'RESIDENT_BINDING', + RESIDENT_ADVISORY: 'RESIDENT_ADVISORY', + STAFF_ONLY: 'STAFF_ONLY', } as const) satisfies Record export type DecisionMode = (typeof DecisionMode)[keyof typeof DecisionMode] export const EndReason = Object.freeze({ - NATURAL: 'NATURAL', - CONFLICT: 'CONFLICT', - REQUEST: 'REQUEST', - CAPACITY: 'CAPACITY', - UPGRADE: 'UPGRADE', - OTHER: 'OTHER', + NATURAL: 'NATURAL', + CONFLICT: 'CONFLICT', + REQUEST: 'REQUEST', + CAPACITY: 'CAPACITY', + UPGRADE: 'UPGRADE', + OTHER: 'OTHER', } as const) satisfies Record export type EndReason = (typeof EndReason)[keyof typeof EndReason] export const EventRsvpStatus = Object.freeze({ - GOING: 'GOING', - MAYBE: 'MAYBE', - DECLINED: 'DECLINED', + GOING: 'GOING', + MAYBE: 'MAYBE', + DECLINED: 'DECLINED', } as const) satisfies Record export type EventRsvpStatus = (typeof EventRsvpStatus)[keyof typeof EventRsvpStatus] export const FamilyStatus = Object.freeze({ - SINGLE: 'SINGLE', - COUPLE: 'COUPLE', - FAMILY_WITH_CHILDREN: 'FAMILY_WITH_CHILDREN', - SINGLE_PARENT: 'SINGLE_PARENT', + SINGLE: 'SINGLE', + COUPLE: 'COUPLE', + FAMILY_WITH_CHILDREN: 'FAMILY_WITH_CHILDREN', + SINGLE_PARENT: 'SINGLE_PARENT', } as const) satisfies Record export type FamilyStatus = (typeof FamilyStatus)[keyof typeof FamilyStatus] export const FollowUpPriority = Object.freeze({ - LOW: 'LOW', - NORMAL: 'NORMAL', - HIGH: 'HIGH', - URGENT: 'URGENT', + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', } as const) satisfies Record export type FollowUpPriority = (typeof FollowUpPriority)[keyof typeof FollowUpPriority] export const Gender = Object.freeze({ - MALE: 'MALE', - FEMALE: 'FEMALE', - OTHER: 'OTHER', - PREFER_NOT_SAY: 'PREFER_NOT_SAY', + MALE: 'MALE', + FEMALE: 'FEMALE', + OTHER: 'OTHER', + PREFER_NOT_SAY: 'PREFER_NOT_SAY', } as const) satisfies Record export type Gender = (typeof Gender)[keyof typeof Gender] export const HouseEventCategory = Object.freeze({ - HOUSE_MEETING: 'HOUSE_MEETING', - SOCIAL: 'SOCIAL', - CULTURE: 'CULTURE', - SUPPORT: 'SUPPORT', + HOUSE_MEETING: 'HOUSE_MEETING', + SOCIAL: 'SOCIAL', + CULTURE: 'CULTURE', + SUPPORT: 'SUPPORT', } as const) satisfies Record export type HouseEventCategory = (typeof HouseEventCategory)[keyof typeof HouseEventCategory] export const HouseEventStatus = Object.freeze({ - DRAFT: 'DRAFT', - PUBLISHED: 'PUBLISHED', - CANCELLED: 'CANCELLED', + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + CANCELLED: 'CANCELLED', } as const) satisfies Record export type HouseEventStatus = (typeof HouseEventStatus)[keyof typeof HouseEventStatus] export const HouseholdTaskCategory = Object.freeze({ - CLEANING: 'CLEANING', - SHOPPING: 'SHOPPING', - MAINTENANCE: 'MAINTENANCE', - COOKING: 'COOKING', - TRASH: 'TRASH', - OTHER: 'OTHER', + CLEANING: 'CLEANING', + SHOPPING: 'SHOPPING', + MAINTENANCE: 'MAINTENANCE', + COOKING: 'COOKING', + TRASH: 'TRASH', + OTHER: 'OTHER', } as const) satisfies Record -export type HouseholdTaskCategory = (typeof HouseholdTaskCategory)[keyof typeof HouseholdTaskCategory] +export type HouseholdTaskCategory = + (typeof HouseholdTaskCategory)[keyof typeof HouseholdTaskCategory] export const HouseholdTaskPriority = Object.freeze({ - LOW: 'LOW', - NORMAL: 'NORMAL', - HIGH: 'HIGH', - URGENT: 'URGENT', + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', } as const) satisfies Record -export type HouseholdTaskPriority = (typeof HouseholdTaskPriority)[keyof typeof HouseholdTaskPriority] +export type HouseholdTaskPriority = + (typeof HouseholdTaskPriority)[keyof typeof HouseholdTaskPriority] export const HouseholdTaskStatus = Object.freeze({ - IDLE: 'IDLE', - NEEDS_ATTENTION: 'NEEDS_ATTENTION', - REQUESTED: 'REQUESTED', - IN_PROGRESS: 'IN_PROGRESS', + IDLE: 'IDLE', + NEEDS_ATTENTION: 'NEEDS_ATTENTION', + REQUESTED: 'REQUESTED', + IN_PROGRESS: 'IN_PROGRESS', } as const) satisfies Record export type HouseholdTaskStatus = (typeof HouseholdTaskStatus)[keyof typeof HouseholdTaskStatus] export const HouseholdTaskType = Object.freeze({ - ONE_TIME: 'ONE_TIME', - RECURRING_SCHEDULED: 'RECURRING_SCHEDULED', - RECURRING_AS_NEEDED: 'RECURRING_AS_NEEDED', + ONE_TIME: 'ONE_TIME', + RECURRING_SCHEDULED: 'RECURRING_SCHEDULED', + RECURRING_AS_NEEDED: 'RECURRING_AS_NEEDED', } as const) satisfies Record export type HouseholdTaskType = (typeof HouseholdTaskType)[keyof typeof HouseholdTaskType] export const HousingStatus = Object.freeze({ - AVAILABLE: 'AVAILABLE', - FULL: 'FULL', - MAINTENANCE: 'MAINTENANCE', - CLOSED: 'CLOSED', + AVAILABLE: 'AVAILABLE', + FULL: 'FULL', + MAINTENANCE: 'MAINTENANCE', + CLOSED: 'CLOSED', } as const) satisfies Record export type HousingStatus = (typeof HousingStatus)[keyof typeof HousingStatus] export const IncidentCategory = Object.freeze({ - INTERPERSONAL: 'INTERPERSONAL', - MAINTENANCE: 'MAINTENANCE', - SAFETY: 'SAFETY', - WELLBEING: 'WELLBEING', + INTERPERSONAL: 'INTERPERSONAL', + MAINTENANCE: 'MAINTENANCE', + SAFETY: 'SAFETY', + WELLBEING: 'WELLBEING', } as const) satisfies Record export type IncidentCategory = (typeof IncidentCategory)[keyof typeof IncidentCategory] export const IncidentSeverity = Object.freeze({ - LOW: 'LOW', - MEDIUM: 'MEDIUM', - HIGH: 'HIGH', - CRITICAL: 'CRITICAL', + LOW: 'LOW', + MEDIUM: 'MEDIUM', + HIGH: 'HIGH', + CRITICAL: 'CRITICAL', } as const) satisfies Record export type IncidentSeverity = (typeof IncidentSeverity)[keyof typeof IncidentSeverity] export const IncidentType = Object.freeze({ - NOISE_COMPLAINT: 'NOISE_COMPLAINT', - CLEANLINESS_DISPUTE: 'CLEANLINESS_DISPUTE', - PERSONAL_CONFLICT: 'PERSONAL_CONFLICT', - CULTURAL_FRICTION: 'CULTURAL_FRICTION', - SPACE_DISPUTE: 'SPACE_DISPUTE', - SCHEDULE_CONFLICT: 'SCHEDULE_CONFLICT', - SAFETY_CONCERN: 'SAFETY_CONCERN', - PLUMBING: 'PLUMBING', - ELECTRICAL: 'ELECTRICAL', - HEATING_COOLING: 'HEATING_COOLING', - APPLIANCE: 'APPLIANCE', - STRUCTURAL: 'STRUCTURAL', - PEST_CONTROL: 'PEST_CONTROL', - SECURITY_SYSTEM: 'SECURITY_SYSTEM', - GENERAL_MAINTENANCE: 'GENERAL_MAINTENANCE', - LOW_SATISFACTION: 'LOW_SATISFACTION', - OTHER: 'OTHER', + NOISE_COMPLAINT: 'NOISE_COMPLAINT', + CLEANLINESS_DISPUTE: 'CLEANLINESS_DISPUTE', + PERSONAL_CONFLICT: 'PERSONAL_CONFLICT', + CULTURAL_FRICTION: 'CULTURAL_FRICTION', + SPACE_DISPUTE: 'SPACE_DISPUTE', + SCHEDULE_CONFLICT: 'SCHEDULE_CONFLICT', + SAFETY_CONCERN: 'SAFETY_CONCERN', + PLUMBING: 'PLUMBING', + ELECTRICAL: 'ELECTRICAL', + HEATING_COOLING: 'HEATING_COOLING', + APPLIANCE: 'APPLIANCE', + STRUCTURAL: 'STRUCTURAL', + PEST_CONTROL: 'PEST_CONTROL', + SECURITY_SYSTEM: 'SECURITY_SYSTEM', + GENERAL_MAINTENANCE: 'GENERAL_MAINTENANCE', + LOW_SATISFACTION: 'LOW_SATISFACTION', + OTHER: 'OTHER', } as const) satisfies Record export type IncidentType = (typeof IncidentType)[keyof typeof IncidentType] export const InvolvementRole = Object.freeze({ - INVOLVED: 'INVOLVED', - WITNESS: 'WITNESS', - MEDIATOR: 'MEDIATOR', + INVOLVED: 'INVOLVED', + WITNESS: 'WITNESS', + MEDIATOR: 'MEDIATOR', } as const) satisfies Record export type InvolvementRole = (typeof InvolvementRole)[keyof typeof InvolvementRole] export const LearningKind = Object.freeze({ - LANGUAGE_TEST: 'LANGUAGE_TEST', - COURSE: 'COURSE', - INFORMAL: 'INFORMAL', - QUALIFICATION: 'QUALIFICATION', - VOLUNTEERING: 'VOLUNTEERING', - COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', - EMPLOYMENT: 'EMPLOYMENT', - INTERNSHIP: 'INTERNSHIP', + LANGUAGE_TEST: 'LANGUAGE_TEST', + COURSE: 'COURSE', + INFORMAL: 'INFORMAL', + QUALIFICATION: 'QUALIFICATION', + VOLUNTEERING: 'VOLUNTEERING', + COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', + EMPLOYMENT: 'EMPLOYMENT', + INTERNSHIP: 'INTERNSHIP', } as const) satisfies Record export type LearningKind = (typeof LearningKind)[keyof typeof LearningKind] export const LearningStatus = Object.freeze({ - PLANNED: 'PLANNED', - IN_PROGRESS: 'IN_PROGRESS', - COMPLETED: 'COMPLETED', - EXPIRED: 'EXPIRED', + PLANNED: 'PLANNED', + IN_PROGRESS: 'IN_PROGRESS', + COMPLETED: 'COMPLETED', + EXPIRED: 'EXPIRED', } as const) satisfies Record export type LearningStatus = (typeof LearningStatus)[keyof typeof LearningStatus] export const LivingSkillsSupport = Object.freeze({ - INDEPENDENT: 'INDEPENDENT', - SOME_SUPPORT: 'SOME_SUPPORT', - REGULAR_SUPPORT: 'REGULAR_SUPPORT', + INDEPENDENT: 'INDEPENDENT', + SOME_SUPPORT: 'SOME_SUPPORT', + REGULAR_SUPPORT: 'REGULAR_SUPPORT', } as const) satisfies Record export type LivingSkillsSupport = (typeof LivingSkillsSupport)[keyof typeof LivingSkillsSupport] export const MaintenanceCategory = Object.freeze({ - PLUMBING: 'PLUMBING', - ELECTRICAL: 'ELECTRICAL', - HEATING_COOLING: 'HEATING_COOLING', - APPLIANCE: 'APPLIANCE', - STRUCTURAL: 'STRUCTURAL', - PEST_CONTROL: 'PEST_CONTROL', - SECURITY: 'SECURITY', - CLEANING: 'CLEANING', - EXTERIOR: 'EXTERIOR', - OTHER: 'OTHER', + PLUMBING: 'PLUMBING', + ELECTRICAL: 'ELECTRICAL', + HEATING_COOLING: 'HEATING_COOLING', + APPLIANCE: 'APPLIANCE', + STRUCTURAL: 'STRUCTURAL', + PEST_CONTROL: 'PEST_CONTROL', + SECURITY: 'SECURITY', + CLEANING: 'CLEANING', + EXTERIOR: 'EXTERIOR', + OTHER: 'OTHER', } as const) satisfies Record export type MaintenanceCategory = (typeof MaintenanceCategory)[keyof typeof MaintenanceCategory] export const MaintenancePriority = Object.freeze({ - LOW: 'LOW', - NORMAL: 'NORMAL', - HIGH: 'HIGH', - URGENT: 'URGENT', + LOW: 'LOW', + NORMAL: 'NORMAL', + HIGH: 'HIGH', + URGENT: 'URGENT', } as const) satisfies Record export type MaintenancePriority = (typeof MaintenancePriority)[keyof typeof MaintenancePriority] export const MaintenanceStatus = Object.freeze({ - OPEN: 'OPEN', - ASSIGNED: 'ASSIGNED', - IN_PROGRESS: 'IN_PROGRESS', - ON_HOLD: 'ON_HOLD', - COMPLETED: 'COMPLETED', - CANCELLED: 'CANCELLED', + OPEN: 'OPEN', + ASSIGNED: 'ASSIGNED', + IN_PROGRESS: 'IN_PROGRESS', + ON_HOLD: 'ON_HOLD', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', } as const) satisfies Record export type MaintenanceStatus = (typeof MaintenanceStatus)[keyof typeof MaintenanceStatus] export const MarketplacePostKind = Object.freeze({ - GIVE_AWAY: 'GIVE_AWAY', - LEND: 'LEND', - WANTED: 'WANTED', - OFFER_HELP: 'OFFER_HELP', - NEED_HELP: 'NEED_HELP', + GIVE_AWAY: 'GIVE_AWAY', + LEND: 'LEND', + WANTED: 'WANTED', + OFFER_HELP: 'OFFER_HELP', + NEED_HELP: 'NEED_HELP', } as const) satisfies Record export type MarketplacePostKind = (typeof MarketplacePostKind)[keyof typeof MarketplacePostKind] export const MarketplacePostStatus = Object.freeze({ - OPEN: 'OPEN', - CLAIMED: 'CLAIMED', - CLOSED: 'CLOSED', + OPEN: 'OPEN', + CLAIMED: 'CLAIMED', + CLOSED: 'CLOSED', } as const) satisfies Record -export type MarketplacePostStatus = (typeof MarketplacePostStatus)[keyof typeof MarketplacePostStatus] +export type MarketplacePostStatus = + (typeof MarketplacePostStatus)[keyof typeof MarketplacePostStatus] export const MedicalDocType = Object.freeze({ - PRIVATE_ROOM: 'PRIVATE_ROOM', - STUDIO: 'STUDIO', - BOTH: 'BOTH', + PRIVATE_ROOM: 'PRIVATE_ROOM', + STUDIO: 'STUDIO', + BOTH: 'BOTH', } as const) satisfies Record export type MedicalDocType = (typeof MedicalDocType)[keyof typeof MedicalDocType] export const MobilityNeed = Object.freeze({ - NONE: 'NONE', - GROUND_FLOOR: 'GROUND_FLOOR', - WHEELCHAIR: 'WHEELCHAIR', + NONE: 'NONE', + GROUND_FLOOR: 'GROUND_FLOOR', + WHEELCHAIR: 'WHEELCHAIR', } as const) satisfies Record export type MobilityNeed = (typeof MobilityNeed)[keyof typeof MobilityNeed] export const OpportunityKind = Object.freeze({ - VOLUNTEERING: 'VOLUNTEERING', - COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', - EMPLOYMENT: 'EMPLOYMENT', - INTERNSHIP: 'INTERNSHIP', + VOLUNTEERING: 'VOLUNTEERING', + COMMUNITY_SERVICE: 'COMMUNITY_SERVICE', + EMPLOYMENT: 'EMPLOYMENT', + INTERNSHIP: 'INTERNSHIP', } as const) satisfies Record export type OpportunityKind = (typeof OpportunityKind)[keyof typeof OpportunityKind] export const OpportunityStatus = Object.freeze({ - DRAFT: 'DRAFT', - PUBLISHED: 'PUBLISHED', - ARCHIVED: 'ARCHIVED', + DRAFT: 'DRAFT', + PUBLISHED: 'PUBLISHED', + ARCHIVED: 'ARCHIVED', } as const) satisfies Record export type OpportunityStatus = (typeof OpportunityStatus)[keyof typeof OpportunityStatus] export const PermitRequirement = Object.freeze({ - NONE: 'NONE', - EMPLOYER_NOTIFIES: 'EMPLOYER_NOTIFIES', - PERMIT_REQUIRED: 'PERMIT_REQUIRED', + NONE: 'NONE', + EMPLOYER_NOTIFIES: 'EMPLOYER_NOTIFIES', + PERMIT_REQUIRED: 'PERMIT_REQUIRED', } as const) satisfies Record export type PermitRequirement = (typeof PermitRequirement)[keyof typeof PermitRequirement] export const PlacementStatus = Object.freeze({ - ACTIVE: 'ACTIVE', - ENDED: 'ENDED', - TRANSFERRED: 'TRANSFERRED', + ACTIVE: 'ACTIVE', + ENDED: 'ENDED', + TRANSFERRED: 'TRANSFERRED', } as const) satisfies Record export type PlacementStatus = (typeof PlacementStatus)[keyof typeof PlacementStatus] export const ProfileVisibility = Object.freeze({ - PRIVATE: 'PRIVATE', - ROOMMATES: 'ROOMMATES', - RESIDENTS: 'RESIDENTS', + PRIVATE: 'PRIVATE', + ROOMMATES: 'ROOMMATES', + RESIDENTS: 'RESIDENTS', } as const) satisfies Record export type ProfileVisibility = (typeof ProfileVisibility)[keyof typeof ProfileVisibility] export const ProposalStatus = Object.freeze({ - DISCUSSION: 'DISCUSSION', - VOTING: 'VOTING', - NEEDS_STAFF_CONFIRMATION: 'NEEDS_STAFF_CONFIRMATION', - ACCEPTED: 'ACCEPTED', - REJECTED: 'REJECTED', - WITHDRAWN: 'WITHDRAWN', - VETOED: 'VETOED', - EXPIRED: 'EXPIRED', + DISCUSSION: 'DISCUSSION', + VOTING: 'VOTING', + NEEDS_STAFF_CONFIRMATION: 'NEEDS_STAFF_CONFIRMATION', + ACCEPTED: 'ACCEPTED', + REJECTED: 'REJECTED', + WITHDRAWN: 'WITHDRAWN', + VETOED: 'VETOED', + EXPIRED: 'EXPIRED', } as const) satisfies Record export type ProposalStatus = (typeof ProposalStatus)[keyof typeof ProposalStatus] export const ProposalType = Object.freeze({ - ADD_RULE: 'ADD_RULE', - AMEND_RULE: 'AMEND_RULE', - REPEAL_RULE: 'REPEAL_RULE', - HOUSE_DECISION: 'HOUSE_DECISION', + ADD_RULE: 'ADD_RULE', + AMEND_RULE: 'AMEND_RULE', + REPEAL_RULE: 'REPEAL_RULE', + HOUSE_DECISION: 'HOUSE_DECISION', } as const) satisfies Record export type ProposalType = (typeof ProposalType)[keyof typeof ProposalType] export const RecyclingKnowledge = Object.freeze({ - NONE: 'NONE', - BASIC: 'BASIC', - GOOD: 'GOOD', + NONE: 'NONE', + BASIC: 'BASIC', + GOOD: 'GOOD', } as const) satisfies Record export type RecyclingKnowledge = (typeof RecyclingKnowledge)[keyof typeof RecyclingKnowledge] export const ResidentOrStaff = Object.freeze({ - RESIDENT: 'RESIDENT', - STAFF: 'STAFF', + RESIDENT: 'RESIDENT', + STAFF: 'STAFF', } as const) satisfies Record export type ResidentOrStaff = (typeof ResidentOrStaff)[keyof typeof ResidentOrStaff] export const ResidentStatus = Object.freeze({ - ACTIVE: 'ACTIVE', - PLACED: 'PLACED', - TRANSFERRED: 'TRANSFERRED', - EXITED: 'EXITED', + ACTIVE: 'ACTIVE', + PLACED: 'PLACED', + TRANSFERRED: 'TRANSFERRED', + EXITED: 'EXITED', } as const) satisfies Record export type ResidentStatus = (typeof ResidentStatus)[keyof typeof ResidentStatus] export const ResolutionStage = Object.freeze({ - REPORTED: 'REPORTED', - SELF_RESOLUTION: 'SELF_RESOLUTION', - PEER_MEDIATION: 'PEER_MEDIATION', - STAFF_MEDIATION: 'STAFF_MEDIATION', - FORMAL_MEASURE: 'FORMAL_MEASURE', - CLOSED: 'CLOSED', + REPORTED: 'REPORTED', + SELF_RESOLUTION: 'SELF_RESOLUTION', + PEER_MEDIATION: 'PEER_MEDIATION', + STAFF_MEDIATION: 'STAFF_MEDIATION', + FORMAL_MEASURE: 'FORMAL_MEASURE', + CLOSED: 'CLOSED', } as const) satisfies Record export type ResolutionStage = (typeof ResolutionStage)[keyof typeof ResolutionStage] export const RoomSharingStatus = Object.freeze({ - CAN_SHARE: 'CAN_SHARE', - PREFERS_PRIVATE: 'PREFERS_PRIVATE', - NEEDS_PRIVATE: 'NEEDS_PRIVATE', + CAN_SHARE: 'CAN_SHARE', + PREFERS_PRIVATE: 'PREFERS_PRIVATE', + NEEDS_PRIVATE: 'NEEDS_PRIVATE', } as const) satisfies Record export type RoomSharingStatus = (typeof RoomSharingStatus)[keyof typeof RoomSharingStatus] export const RuleCategory = Object.freeze({ - SAFETY: 'SAFETY', - RESPECT: 'RESPECT', - NOISE: 'NOISE', - CLEANLINESS: 'CLEANLINESS', - KITCHEN: 'KITCHEN', - BATHROOM: 'BATHROOM', - GUESTS: 'GUESTS', - SHARED_SPACES: 'SHARED_SPACES', - COSTS: 'COSTS', - COMMUNICATION: 'COMMUNICATION', - OTHER: 'OTHER', + SAFETY: 'SAFETY', + RESPECT: 'RESPECT', + NOISE: 'NOISE', + CLEANLINESS: 'CLEANLINESS', + KITCHEN: 'KITCHEN', + BATHROOM: 'BATHROOM', + GUESTS: 'GUESTS', + SHARED_SPACES: 'SHARED_SPACES', + COSTS: 'COSTS', + COMMUNICATION: 'COMMUNICATION', + OTHER: 'OTHER', } as const) satisfies Record export type RuleCategory = (typeof RuleCategory)[keyof typeof RuleCategory] export const RuleDelegation = Object.freeze({ - FIXED: 'FIXED', - UNIT_MAY_STRENGTHEN: 'UNIT_MAY_STRENGTHEN', - UNIT_DECIDES: 'UNIT_DECIDES', + FIXED: 'FIXED', + UNIT_MAY_STRENGTHEN: 'UNIT_MAY_STRENGTHEN', + UNIT_DECIDES: 'UNIT_DECIDES', } as const) satisfies Record export type RuleDelegation = (typeof RuleDelegation)[keyof typeof RuleDelegation] export const RuleScope = Object.freeze({ - ORG: 'ORG', - UNIT: 'UNIT', + ORG: 'ORG', + UNIT: 'UNIT', } as const) satisfies Record export type RuleScope = (typeof RuleScope)[keyof typeof RuleScope] export const RuleStatus = Object.freeze({ - ACTIVE: 'ACTIVE', - SUPERSEDED: 'SUPERSEDED', - ARCHIVED: 'ARCHIVED', + ACTIVE: 'ACTIVE', + SUPERSEDED: 'SUPERSEDED', + ARCHIVED: 'ARCHIVED', } as const) satisfies Record export type RuleStatus = (typeof RuleStatus)[keyof typeof RuleStatus] export const SleepSchedule = Object.freeze({ - EARLY_BIRD: 'EARLY_BIRD', - STANDARD: 'STANDARD', - NIGHT_OWL: 'NIGHT_OWL', - IRREGULAR: 'IRREGULAR', + EARLY_BIRD: 'EARLY_BIRD', + STANDARD: 'STANDARD', + NIGHT_OWL: 'NIGHT_OWL', + IRREGULAR: 'IRREGULAR', } as const) satisfies Record export type SleepSchedule = (typeof SleepSchedule)[keyof typeof SleepSchedule] export const SmokingStatus = Object.freeze({ - NON_SMOKER: 'NON_SMOKER', - OUTDOOR_SMOKER: 'OUTDOOR_SMOKER', - INDOOR_SMOKER: 'INDOOR_SMOKER', + NON_SMOKER: 'NON_SMOKER', + OUTDOOR_SMOKER: 'OUTDOOR_SMOKER', + INDOOR_SMOKER: 'INDOOR_SMOKER', } as const) satisfies Record export type SmokingStatus = (typeof SmokingStatus)[keyof typeof SmokingStatus] export const SocialStyle = Object.freeze({ - INTROVERTED: 'INTROVERTED', - MODERATE: 'MODERATE', - EXTROVERTED: 'EXTROVERTED', + INTROVERTED: 'INTROVERTED', + MODERATE: 'MODERATE', + EXTROVERTED: 'EXTROVERTED', } as const) satisfies Record export type SocialStyle = (typeof SocialStyle)[keyof typeof SocialStyle] export const SpotStatus = Object.freeze({ - AVAILABLE: 'AVAILABLE', - OCCUPIED: 'OCCUPIED', - MAINTENANCE: 'MAINTENANCE', - CLOSED: 'CLOSED', + AVAILABLE: 'AVAILABLE', + OCCUPIED: 'OCCUPIED', + MAINTENANCE: 'MAINTENANCE', + CLOSED: 'CLOSED', } as const) satisfies Record export type SpotStatus = (typeof SpotStatus)[keyof typeof SpotStatus] export const SpotType = Object.freeze({ - BED: 'BED', - PRIVATE_ROOM: 'PRIVATE_ROOM', - STUDIO: 'STUDIO', - ROOM: 'ROOM', + BED: 'BED', + PRIVATE_ROOM: 'PRIVATE_ROOM', + STUDIO: 'STUDIO', + ROOM: 'ROOM', } as const) satisfies Record export type SpotType = (typeof SpotType)[keyof typeof SpotType] export const StaffDecision = Object.freeze({ - CONFIRMED: 'CONFIRMED', - VETOED: 'VETOED', + CONFIRMED: 'CONFIRMED', + VETOED: 'VETOED', } as const) satisfies Record export type StaffDecision = (typeof StaffDecision)[keyof typeof StaffDecision] export const StaffRole = Object.freeze({ - ADMIN: 'ADMIN', - BETREUUNG: 'BETREUUNG', - SOZIALARBEIT: 'SOZIALARBEIT', - JOBCOACH: 'JOBCOACH', - FREIWILLIGENARBEIT: 'FREIWILLIGENARBEIT', + ADMIN: 'ADMIN', + BETREUUNG: 'BETREUUNG', + SOZIALARBEIT: 'SOZIALARBEIT', + JOBCOACH: 'JOBCOACH', + FREIWILLIGENARBEIT: 'FREIWILLIGENARBEIT', } as const) satisfies Record export type StaffRole = (typeof StaffRole)[keyof typeof StaffRole] export const StaffScope = Object.freeze({ - OWN_DOMAIN: 'OWN_DOMAIN', - ALL_DOMAINS: 'ALL_DOMAINS', + OWN_DOMAIN: 'OWN_DOMAIN', + ALL_DOMAINS: 'ALL_DOMAINS', } as const) satisfies Record export type StaffScope = (typeof StaffScope)[keyof typeof StaffScope] export const SupportLevel = Object.freeze({ - STANDARD: 'STANDARD', - ELEVATED: 'ELEVATED', - INTENSIVE: 'INTENSIVE', + STANDARD: 'STANDARD', + ELEVATED: 'ELEVATED', + INTENSIVE: 'INTENSIVE', } as const) satisfies Record export type SupportLevel = (typeof SupportLevel)[keyof typeof SupportLevel] export const TaskRequestStatus = Object.freeze({ - PENDING: 'PENDING', - ACCEPTED: 'ACCEPTED', - DECLINED: 'DECLINED', - COMPLETED: 'COMPLETED', + PENDING: 'PENDING', + ACCEPTED: 'ACCEPTED', + DECLINED: 'DECLINED', + COMPLETED: 'COMPLETED', } as const) satisfies Record export type TaskRequestStatus = (typeof TaskRequestStatus)[keyof typeof TaskRequestStatus] export const TransferRequestStatus = Object.freeze({ - PENDING: 'PENDING', - APPROVED: 'APPROVED', - DENIED: 'DENIED', - COMPLETED: 'COMPLETED', - CANCELLED: 'CANCELLED', + PENDING: 'PENDING', + APPROVED: 'APPROVED', + DENIED: 'DENIED', + COMPLETED: 'COMPLETED', + CANCELLED: 'CANCELLED', } as const) satisfies Record -export type TransferRequestStatus = (typeof TransferRequestStatus)[keyof typeof TransferRequestStatus] +export type TransferRequestStatus = + (typeof TransferRequestStatus)[keyof typeof TransferRequestStatus] export const VoteChoice = Object.freeze({ - YES: 'YES', - NO: 'NO', - ABSTAIN: 'ABSTAIN', - BLOCK: 'BLOCK', + YES: 'YES', + NO: 'NO', + ABSTAIN: 'ABSTAIN', + BLOCK: 'BLOCK', } as const) satisfies Record export type VoteChoice = (typeof VoteChoice)[keyof typeof VoteChoice] export const VoteThreshold = Object.freeze({ - CONSENSUS: 'CONSENSUS', - SUPERMAJORITY: 'SUPERMAJORITY', - SIMPLE_MAJORITY: 'SIMPLE_MAJORITY', + CONSENSUS: 'CONSENSUS', + SUPERMAJORITY: 'SUPERMAJORITY', + SIMPLE_MAJORITY: 'SIMPLE_MAJORITY', } as const) satisfies Record export type VoteThreshold = (typeof VoteThreshold)[keyof typeof VoteThreshold] - +export const SiteAccess = Object.freeze({ + ALL_UNITS: 'ALL_UNITS', + ASSIGNED_UNITS: 'ASSIGNED_UNITS', +} as const) satisfies Record +export type SiteAccess = (typeof SiteAccess)[keyof typeof SiteAccess] From 12f400447828c6849ce81830814236f91e7437ff Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:41:08 +0200 Subject: [PATCH 5/5] refactor(db): port #153 (interpreter need) to Drizzle - enum, column, 0002 Same drill as #152: InterpreterNeed pgEnum + Resident.interpreterNeed (default NONE) + Resident_interpreterNeed_idx, drizzle/0002 matching the Prisma migration statement for statement, InterpreterNeed runtime enum object in types.ts, and the three new imports repointed at @/lib/db. Scratch parity re-proven: full Prisma chain (31) vs drizzle 0000-0002, normalized pg_dump diff EMPTY. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqKqMnHQHSmkGFfc5t7Rxn --- drizzle/0002_interpreter_need.sql | 3 + drizzle/meta/0002_snapshot.json | 7609 +++++++++++++++++ drizzle/meta/_journal.json | 7 + src/components/residents/CareWorkspace.tsx | 2 +- .../__tests__/CareWorkspace.test.tsx | 2 +- src/lib/config/interpreting.ts | 2 +- src/lib/db/schema.ts | 3 + src/lib/db/types.ts | 6 + 8 files changed, 7631 insertions(+), 3 deletions(-) create mode 100644 drizzle/0002_interpreter_need.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_interpreter_need.sql b/drizzle/0002_interpreter_need.sql new file mode 100644 index 00000000..082195fa --- /dev/null +++ b/drizzle/0002_interpreter_need.sql @@ -0,0 +1,3 @@ +CREATE TYPE "public"."InterpreterNeed" AS ENUM('NONE', 'FOR_COMPLEX', 'ALWAYS');--> statement-breakpoint +ALTER TABLE "Resident" ADD COLUMN "interpreterNeed" "InterpreterNeed" DEFAULT 'NONE' NOT NULL;--> statement-breakpoint +CREATE INDEX "Resident_interpreterNeed_idx" ON "Resident" USING btree ("interpreterNeed"); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..91d18013 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,7609 @@ +{ + "id": "ea0546d2-64a5-4752-8c6c-f53c21884b4e", + "prevId": "f57a3251-c84e-4bb5-b4b4-b4d90bf08291", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Account": { + "name": "Account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerifiedAt": { + "name": "emailVerifiedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Account_email_key": { + "name": "Account_email_key", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_idx": { + "name": "Account_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_residentId_key": { + "name": "Account_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_idx": { + "name": "Account_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Account_userId_key": { + "name": "Account_userId_key", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Account_userId_fkey": { + "name": "Account_userId_fkey", + "tableFrom": "Account", + "tableTo": "User", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Account_residentId_fkey": { + "name": "Account_residentId_fkey", + "tableFrom": "Account", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Activity": { + "name": "Activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "ActivityCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "ActivityCost", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FREE'" + }, + "costNote": { + "name": "costNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ActivityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "highlight": { + "name": "highlight", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Activity_endsAt_idx": { + "name": "Activity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_category_idx": { + "name": "Activity_status_category_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Activity_status_highlight_idx": { + "name": "Activity_status_highlight_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "highlight", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Activity_createdByUserId_fkey": { + "name": "Activity_createdByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Activity_updatedByUserId_fkey": { + "name": "Activity_updatedByUserId_fkey", + "tableFrom": "Activity", + "tableTo": "User", + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AgreementParty": { + "name": "AgreementParty", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agreementId": { + "name": "agreementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acceptedAt": { + "name": "acceptedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "declinedAt": { + "name": "declinedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AgreementParty_agreementId_residentId_key": { + "name": "AgreementParty_agreementId_residentId_key", + "columns": [ + { + "expression": "agreementId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AgreementParty_residentId_idx": { + "name": "AgreementParty_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AgreementParty_agreementId_fkey": { + "name": "AgreementParty_agreementId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "ConflictAgreement", + "columnsFrom": ["agreementId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "AgreementParty_residentId_fkey": { + "name": "AgreementParty_residentId_fkey", + "tableFrom": "AgreementParty", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AlgorithmWeight": { + "name": "AlgorithmWeight", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "lifestyleWeight": { + "name": "lifestyleWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "socialWeight": { + "name": "socialWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "practicalWeight": { + "name": "practicalWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "riskWeight": { + "name": "riskWeight", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "factorWeights": { + "name": "factorWeights", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AlgorithmWeight_active_idx": { + "name": "AlgorithmWeight_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Appointment": { + "name": "Appointment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "AppointmentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SCHEDULED'" + }, + "residentNote": { + "name": "residentNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffNote": { + "name": "staffNote", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Appointment_residentId_startsAt_idx": { + "name": "Appointment_residentId_startsAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_staffId_startsAt_idx": { + "name": "Appointment_staffId_startsAt_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_domain_idx": { + "name": "Appointment_status_domain_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Appointment_status_idx": { + "name": "Appointment_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Appointment_residentId_fkey": { + "name": "Appointment_residentId_fkey", + "tableFrom": "Appointment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Appointment_staffId_fkey": { + "name": "Appointment_staffId_fkey", + "tableFrom": "Appointment", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuditLog": { + "name": "AuditLog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entityId": { + "name": "entityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "AuditLog_createdAt_idx": { + "name": "AuditLog_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_entity_entityId_idx": { + "name": "AuditLog_entity_entityId_idx", + "columns": [ + { + "expression": "entity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuditLog_userId_idx": { + "name": "AuditLog_userId_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuditLog_userId_fkey": { + "name": "AuditLog_userId_fkey", + "tableFrom": "AuditLog", + "tableTo": "User", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.AuthToken": { + "name": "AuthToken", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "tokenHash": { + "name": "tokenHash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "AuthTokenPurpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "usedAt": { + "name": "usedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "AuthToken_accountId_purpose_idx": { + "name": "AuthToken_accountId_purpose_idx", + "columns": [ + { + "expression": "accountId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purpose", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "AuthToken_tokenHash_key": { + "name": "AuthToken_tokenHash_key", + "columns": [ + { + "expression": "tokenHash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "AuthToken_accountId_fkey": { + "name": "AuthToken_accountId_fkey", + "tableFrom": "AuthToken", + "tableTo": "Account", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAssignment": { + "name": "CareAssignment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAssignment_residentId_role_key": { + "name": "CareAssignment_residentId_role_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAssignment_staffId_idx": { + "name": "CareAssignment_staffId_idx", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAssignment_residentId_fkey": { + "name": "CareAssignment_residentId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAssignment_staffId_fkey": { + "name": "CareAssignment_staffId_fkey", + "tableFrom": "CareAssignment", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CareAttribute": { + "name": "CareAttribute", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "CareRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedById": { + "name": "updatedById", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "CareAttribute_residentId_domain_idx": { + "name": "CareAttribute_residentId_domain_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CareAttribute_residentId_domain_key_key": { + "name": "CareAttribute_residentId_domain_key_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CareAttribute_residentId_fkey": { + "name": "CareAttribute_residentId_fkey", + "tableFrom": "CareAttribute", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CareAttribute_updatedById_fkey": { + "name": "CareAttribute_updatedById_fkey", + "tableFrom": "CareAttribute", + "tableTo": "User", + "columnsFrom": ["updatedById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.CompatibilityAssessment": { + "name": "CompatibilityAssessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparedWithId": { + "name": "comparedWithId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overallScore": { + "name": "overallScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "strengths": { + "name": "strengths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "recommendations": { + "name": "recommendations", + "type": "text[]", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "CompatibilityAssessment_overallScore_idx": { + "name": "CompatibilityAssessment_overallScore_idx", + "columns": [ + { + "expression": "overallScore", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "CompatibilityAssessment_residentId_comparedWithId_key": { + "name": "CompatibilityAssessment_residentId_comparedWithId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "comparedWithId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "CompatibilityAssessment_residentId_fkey": { + "name": "CompatibilityAssessment_residentId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "CompatibilityAssessment_comparedWithId_fkey": { + "name": "CompatibilityAssessment_comparedWithId_fkey", + "tableFrom": "CompatibilityAssessment", + "tableTo": "Resident", + "columnsFrom": ["comparedWithId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Complaint": { + "name": "Complaint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "ComplaintSubject", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ComplaintStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "respondedAt": { + "name": "respondedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "respondedByUserId": { + "name": "respondedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Complaint_createdAt_idx": { + "name": "Complaint_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_residentId_idx": { + "name": "Complaint_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Complaint_status_idx": { + "name": "Complaint_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Complaint_residentId_fkey": { + "name": "Complaint_residentId_fkey", + "tableFrom": "Complaint", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Complaint_respondedByUserId_fkey": { + "name": "Complaint_respondedByUserId_fkey", + "tableFrom": "Complaint", + "tableTo": "User", + "columnsFrom": ["respondedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ConflictAgreement": { + "name": "ConflictAgreement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms": { + "name": "terms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mediatorName": { + "name": "mediatorName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewDate": { + "name": "reviewDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "AgreementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PROPOSED'" + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "ruleProposalId": { + "name": "ruleProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ConflictAgreement_incidentId_idx": { + "name": "ConflictAgreement_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_ruleProposalId_key": { + "name": "ConflictAgreement_ruleProposalId_key", + "columns": [ + { + "expression": "ruleProposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ConflictAgreement_status_reviewDate_idx": { + "name": "ConflictAgreement_status_reviewDate_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reviewDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ConflictAgreement_incidentId_fkey": { + "name": "ConflictAgreement_incidentId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ConflictAgreement_ruleProposalId_fkey": { + "name": "ConflictAgreement_ruleProposalId_fkey", + "tableFrom": "ConflictAgreement", + "tableTo": "Proposal", + "columnsFrom": ["ruleProposalId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EventRsvp": { + "name": "EventRsvp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "EventRsvpStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GOING'" + } + }, + "indexes": { + "EventRsvp_eventId_idx": { + "name": "EventRsvp_eventId_idx", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EventRsvp_eventId_residentId_key": { + "name": "EventRsvp_eventId_residentId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EventRsvp_eventId_fkey": { + "name": "EventRsvp_eventId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "HouseEvent", + "columnsFrom": ["eventId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EventRsvp_residentId_fkey": { + "name": "EventRsvp_residentId_fkey", + "tableFrom": "EventRsvp", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Expense": { + "name": "Expense", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paidById": { + "name": "paidById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdById": { + "name": "createdById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Expense_housingUnitId_date_idx": { + "name": "Expense_housingUnitId_date_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Expense_housingUnitId_fkey": { + "name": "Expense_housingUnitId_fkey", + "tableFrom": "Expense", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Expense_paidById_fkey": { + "name": "Expense_paidById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": ["paidById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Expense_createdById_fkey": { + "name": "Expense_createdById_fkey", + "tableFrom": "Expense", + "tableTo": "Resident", + "columnsFrom": ["createdById"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ExpenseShare": { + "name": "ExpenseShare", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expenseId": { + "name": "expenseId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ExpenseShare_expenseId_residentId_key": { + "name": "ExpenseShare_expenseId_residentId_key", + "columns": [ + { + "expression": "expenseId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ExpenseShare_residentId_idx": { + "name": "ExpenseShare_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ExpenseShare_expenseId_fkey": { + "name": "ExpenseShare_expenseId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Expense", + "columnsFrom": ["expenseId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ExpenseShare_residentId_fkey": { + "name": "ExpenseShare_residentId_fkey", + "tableFrom": "ExpenseShare", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseEvent": { + "name": "HouseEvent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "HouseEventCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SOCIAL'" + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "HouseEventStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLISHED'" + }, + "createdByStaffId": { + "name": "createdByStaffId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseEvent_housingUnitId_startsAt_idx": { + "name": "HouseEvent_housingUnitId_startsAt_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseEvent_status_startsAt_idx": { + "name": "HouseEvent_status_startsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseEvent_housingUnitId_fkey": { + "name": "HouseEvent_housingUnitId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseEvent_createdByStaffId_fkey": { + "name": "HouseEvent_createdByStaffId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "User", + "columnsFrom": ["createdByStaffId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "HouseEvent_createdByResidentId_fkey": { + "name": "HouseEvent_createdByResidentId_fkey", + "tableFrom": "HouseEvent", + "tableTo": "Resident", + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseRule": { + "name": "HouseRule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "RuleScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delegation": { + "name": "delegation", + "type": "RuleDelegation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'FIXED'" + }, + "parentRuleId": { + "name": "parentRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "RuleStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "effectiveFrom": { + "name": "effectiveFrom", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "effectiveUntil": { + "name": "effectiveUntil", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "adoptedByProposalId": { + "name": "adoptedByProposalId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HouseRule_category_idx": { + "name": "HouseRule_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_housingUnitId_status_idx": { + "name": "HouseRule_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_key_key": { + "name": "HouseRule_key_key", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_parentRuleId_idx": { + "name": "HouseRule_parentRuleId_idx", + "columns": [ + { + "expression": "parentRuleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseRule_scope_status_idx": { + "name": "HouseRule_scope_status_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseRule_housingUnitId_fkey": { + "name": "HouseRule_housingUnitId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseRule_parentRuleId_fkey": { + "name": "HouseRule_parentRuleId_fkey", + "tableFrom": "HouseRule", + "tableTo": "HouseRule", + "columnsFrom": ["parentRuleId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "HouseRule_adoptedByProposalId_fkey": { + "name": "HouseRule_adoptedByProposalId_fkey", + "tableFrom": "HouseRule", + "tableTo": "Proposal", + "columnsFrom": ["adoptedByProposalId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HouseholdTask": { + "name": "HouseholdTask", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instructions": { + "name": "instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taskType": { + "name": "taskType", + "type": "HouseholdTaskType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ONE_TIME'" + }, + "category": { + "name": "category", + "type": "HouseholdTaskCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "priority": { + "name": "priority", + "type": "HouseholdTaskPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "scheduleHuman": { + "name": "scheduleHuman", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "estimatedMinutes": { + "name": "estimatedMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currentStatus": { + "name": "currentStatus", + "type": "HouseholdTaskStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'IDLE'" + }, + "isCompleted": { + "name": "isCompleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByResidentId": { + "name": "createdByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdByStaff": { + "name": "createdByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checklist": { + "name": "checklist", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + }, + "rotationResidentIds": { + "name": "rotationResidentIds", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "HouseholdTask_housingUnitId_category_idx": { + "name": "HouseholdTask_housingUnitId_category_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HouseholdTask_housingUnitId_currentStatus_idx": { + "name": "HouseholdTask_housingUnitId_currentStatus_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currentStatus", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "HouseholdTask_housingUnitId_fkey": { + "name": "HouseholdTask_housingUnitId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "HouseholdTask_createdByResidentId_fkey": { + "name": "HouseholdTask_createdByResidentId_fkey", + "tableFrom": "HouseholdTask", + "tableTo": "Resident", + "columnsFrom": ["createdByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.HousingUnit": { + "name": "HousingUnit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totalBeds": { + "name": "totalBeds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "totalRooms": { + "name": "totalRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedRooms": { + "name": "sharedRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateRooms": { + "name": "privateRooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedBathrooms": { + "name": "sharedBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "privateBathrooms": { + "name": "privateBathrooms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privateKitchen": { + "name": "privateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "groundFloor": { + "name": "groundFloor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "wheelchairAccess": { + "name": "wheelchairAccess", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elevator": { + "name": "elevator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "smokingAllowed": { + "name": "smokingAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petsAllowed": { + "name": "petsAllowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "quietHours": { + "name": "quietHours", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nearPublicTransport": { + "name": "nearPublicTransport", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "nearHealthServices": { + "name": "nearHealthServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "nearSchools": { + "name": "nearSchools", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "HousingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildingCode": { + "name": "buildingCode", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "HousingUnit_buildingCode_idx": { + "name": "HousingUnit_buildingCode_idx", + "columns": [ + { + "expression": "buildingCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_code_key": { + "name": "HousingUnit_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_status_idx": { + "name": "HousingUnit_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "HousingUnit_totalBeds_idx": { + "name": "HousingUnit_totalBeds_idx", + "columns": [ + { + "expression": "totalBeds", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Incident": { + "name": "Incident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subjectId": { + "name": "subjectId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "IncidentCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERPERSONAL'" + }, + "type": { + "name": "type", + "type": "IncidentType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "IncidentSeverity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "predictable": { + "name": "predictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "compatibilityGap": { + "name": "compatibilityGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nextFollowUpDate": { + "name": "nextFollowUpDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "followUpPriority": { + "name": "followUpPriority", + "type": "FollowUpPriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "mediationMinutes": { + "name": "mediationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolutionStage": { + "name": "resolutionStage", + "type": "ResolutionStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'REPORTED'" + }, + "stageEnteredAt": { + "name": "stageEnteredAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Incident_date_idx": { + "name": "Incident_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_nextFollowUpDate_idx": { + "name": "Incident_nextFollowUpDate_idx", + "columns": [ + { + "expression": "nextFollowUpDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_reportedById_idx": { + "name": "Incident_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_subjectId_idx": { + "name": "Incident_subjectId_idx", + "columns": [ + { + "expression": "subjectId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Incident_type_severity_idx": { + "name": "Incident_type_severity_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Incident_housingUnitId_fkey": { + "name": "Incident_housingUnitId_fkey", + "tableFrom": "Incident", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Incident_placementId_fkey": { + "name": "Incident_placementId_fkey", + "tableFrom": "Incident", + "tableTo": "Placement", + "columnsFrom": ["placementId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_reportedById_fkey": { + "name": "Incident_reportedById_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Incident_subjectId_fkey": { + "name": "Incident_subjectId_fkey", + "tableFrom": "Incident", + "tableTo": "Resident", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentFollowUp": { + "name": "IncidentFollowUp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffName": { + "name": "staffName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduledNextDate": { + "name": "scheduledNextDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IncidentFollowUp_createdAt_idx": { + "name": "IncidentFollowUp_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentFollowUp_incidentId_idx": { + "name": "IncidentFollowUp_incidentId_idx", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentFollowUp_incidentId_fkey": { + "name": "IncidentFollowUp_incidentId_fkey", + "tableFrom": "IncidentFollowUp", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IncidentInvolvement": { + "name": "IncidentInvolvement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "incidentId": { + "name": "incidentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "InvolvementRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INVOLVED'" + } + }, + "indexes": { + "IncidentInvolvement_incidentId_residentId_key": { + "name": "IncidentInvolvement_incidentId_residentId_key", + "columns": [ + { + "expression": "incidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IncidentInvolvement_residentId_idx": { + "name": "IncidentInvolvement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IncidentInvolvement_incidentId_fkey": { + "name": "IncidentInvolvement_incidentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Incident", + "columnsFrom": ["incidentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "IncidentInvolvement_residentId_fkey": { + "name": "IncidentInvolvement_residentId_fkey", + "tableFrom": "IncidentInvolvement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.LearningRecord": { + "name": "LearningRecord", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "LearningKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "LearningStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "languageCode": { + "name": "languageCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cefrLevel": { + "name": "cefrLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recordedBy": { + "name": "recordedBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LearningRecord_languageCode_cefrLevel_idx": { + "name": "LearningRecord_languageCode_cefrLevel_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cefrLevel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_residentId_kind_idx": { + "name": "LearningRecord_residentId_kind_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "LearningRecord_status_idx": { + "name": "LearningRecord_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "LearningRecord_residentId_fkey": { + "name": "LearningRecord_residentId_fkey", + "tableFrom": "LearningRecord", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MaintenanceRequest": { + "name": "MaintenanceRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "MaintenanceCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "MaintenancePriority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reportedById": { + "name": "reportedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reporterName": { + "name": "reporterName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedTo": { + "name": "assignedTo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "MaintenanceStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MaintenanceRequest_createdAt_idx": { + "name": "MaintenanceRequest_createdAt_idx", + "columns": [ + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_housingUnitId_idx": { + "name": "MaintenanceRequest_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_priority_status_idx": { + "name": "MaintenanceRequest_priority_status_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_reportedById_idx": { + "name": "MaintenanceRequest_reportedById_idx", + "columns": [ + { + "expression": "reportedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MaintenanceRequest_status_idx": { + "name": "MaintenanceRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MaintenanceRequest_housingUnitId_fkey": { + "name": "MaintenanceRequest_housingUnitId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MaintenanceRequest_spotId_fkey": { + "name": "MaintenanceRequest_spotId_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "PlacementSpot", + "columnsFrom": ["spotId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "MaintenanceRequest_reportedById_fkey": { + "name": "MaintenanceRequest_reportedById_fkey", + "tableFrom": "MaintenanceRequest", + "tableTo": "Resident", + "columnsFrom": ["reportedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MarketplacePost": { + "name": "MarketplacePost", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postedById": { + "name": "postedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "MarketplacePostKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "status": { + "name": "status", + "type": "MarketplacePostStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "claimedById": { + "name": "claimedById", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "closedAt": { + "name": "closedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "hiddenByStaff": { + "name": "hiddenByStaff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hiddenReason": { + "name": "hiddenReason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactNote": { + "name": "contactNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimedAt": { + "name": "claimedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "MarketplacePost_housingUnitId_status_idx": { + "name": "MarketplacePost_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MarketplacePost_postedById_idx": { + "name": "MarketplacePost_postedById_idx", + "columns": [ + { + "expression": "postedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MarketplacePost_housingUnitId_fkey": { + "name": "MarketplacePost_housingUnitId_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_postedById_fkey": { + "name": "MarketplacePost_postedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": ["postedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MarketplacePost_claimedById_fkey": { + "name": "MarketplacePost_claimedById_fkey", + "tableFrom": "MarketplacePost", + "tableTo": "Resident", + "columnsFrom": ["claimedById"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Message": { + "name": "Message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "threadId": { + "name": "threadId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorResidentId": { + "name": "authorResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorUserId": { + "name": "authorUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "readAt": { + "name": "readAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Message_threadId_createdAt_idx": { + "name": "Message_threadId_createdAt_idx", + "columns": [ + { + "expression": "threadId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Message_threadId_fkey": { + "name": "Message_threadId_fkey", + "tableFrom": "Message", + "tableTo": "MessageThread", + "columnsFrom": ["threadId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Message_authorResidentId_fkey": { + "name": "Message_authorResidentId_fkey", + "tableFrom": "Message", + "tableTo": "Resident", + "columnsFrom": ["authorResidentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Message_authorUserId_fkey": { + "name": "Message_authorUserId_fkey", + "tableFrom": "Message", + "tableTo": "User", + "columnsFrom": ["authorUserId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "Message_one_author": { + "name": "Message_one_author", + "value": "(\"authorResidentId\" IS NOT NULL) <> (\"authorUserId\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.MessageThread": { + "name": "MessageThread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "MessageThread_residentId_key": { + "name": "MessageThread_residentId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MessageThread_updatedAt_idx": { + "name": "MessageThread_updatedAt_idx", + "columns": [ + { + "expression": "updatedAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MessageThread_residentId_fkey": { + "name": "MessageThread_residentId_fkey", + "tableFrom": "MessageThread", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Opportunity": { + "name": "Opportunity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "OpportunityKind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organisation": { + "name": "organisation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hoursPerWeek": { + "name": "hoursPerWeek", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "germanLevel": { + "name": "germanLevel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permitRequirement": { + "name": "permitRequirement", + "type": "PermitRequirement", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "requirementNote": { + "name": "requirementNote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactName": { + "name": "contactName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactEmail": { + "name": "contactEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactPhone": { + "name": "contactPhone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "OpportunityStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "startsAt": { + "name": "startsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "endsAt": { + "name": "endsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "createdByUserId": { + "name": "createdByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updatedByUserId": { + "name": "updatedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Opportunity_endsAt_idx": { + "name": "Opportunity_endsAt_idx", + "columns": [ + { + "expression": "endsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Opportunity_status_kind_idx": { + "name": "Opportunity_status_kind_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Opportunity_createdByUserId_fkey": { + "name": "Opportunity_createdByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": ["createdByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Opportunity_updatedByUserId_fkey": { + "name": "Opportunity_updatedByUserId_fkey", + "tableFrom": "Opportunity", + "tableTo": "User", + "columnsFrom": ["updatedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpportunityApplication": { + "name": "OpportunityApplication", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opportunityId": { + "name": "opportunityId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stage": { + "name": "stage", + "type": "ApplicationStage", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INTERESTED'" + }, + "stageChangedAt": { + "name": "stageChangedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "ResidentOrStaff", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supportedByUserId": { + "name": "supportedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "learningRecordId": { + "name": "learningRecordId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "OpportunityApplication_learningRecordId_key": { + "name": "OpportunityApplication_learningRecordId_key", + "columns": [ + { + "expression": "learningRecordId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_opportunityId_stage_idx": { + "name": "OpportunityApplication_opportunityId_stage_idx", + "columns": [ + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_idx": { + "name": "OpportunityApplication_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_residentId_opportunityId_key": { + "name": "OpportunityApplication_residentId_opportunityId_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opportunityId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "OpportunityApplication_stage_idx": { + "name": "OpportunityApplication_stage_idx", + "columns": [ + { + "expression": "stage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "OpportunityApplication_residentId_fkey": { + "name": "OpportunityApplication_residentId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "OpportunityApplication_opportunityId_fkey": { + "name": "OpportunityApplication_opportunityId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "Opportunity", + "columnsFrom": ["opportunityId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "OpportunityApplication_supportedByUserId_fkey": { + "name": "OpportunityApplication_supportedByUserId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "User", + "columnsFrom": ["supportedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "OpportunityApplication_learningRecordId_fkey": { + "name": "OpportunityApplication_learningRecordId_fkey", + "tableFrom": "OpportunityApplication", + "tableTo": "LearningRecord", + "columnsFrom": ["learningRecordId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Placement": { + "name": "Placement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spotId": { + "name": "spotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "compatibilityScore": { + "name": "compatibilityScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "lifestyleScore": { + "name": "lifestyleScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "socialScore": { + "name": "socialScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "practicalScore": { + "name": "practicalScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "riskScore": { + "name": "riskScore", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "PlacementStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "endReason": { + "name": "endReason", + "type": "EndReason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "satisfactionRating": { + "name": "satisfactionRating", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "placementNotes": { + "name": "placementNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcomeNotes": { + "name": "outcomeNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictGap": { + "name": "conflictGap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wasPredictable": { + "name": "wasPredictable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "relatedIncidentId": { + "name": "relatedIncidentId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Placement_housingUnitId_idx": { + "name": "Placement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_housingUnitId_startDate_key": { + "name": "Placement_residentId_housingUnitId_startDate_key", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_residentId_idx": { + "name": "Placement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_startDate_endDate_idx": { + "name": "Placement_startDate_endDate_idx", + "columns": [ + { + "expression": "startDate", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "endDate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Placement_status_idx": { + "name": "Placement_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Placement_residentId_fkey": { + "name": "Placement_residentId_fkey", + "tableFrom": "Placement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_housingUnitId_fkey": { + "name": "Placement_housingUnitId_fkey", + "tableFrom": "Placement", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Placement_spotId_fkey": { + "name": "Placement_spotId_fkey", + "tableFrom": "Placement", + "tableTo": "PlacementSpot", + "columnsFrom": ["spotId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Placement_relatedIncidentId_fkey": { + "name": "Placement_relatedIncidentId_fkey", + "tableFrom": "Placement", + "tableTo": "Incident", + "columnsFrom": ["relatedIncidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PlacementSpot": { + "name": "PlacementSpot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "SpotType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parentSpotId": { + "name": "parentSpotId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "squareMeters": { + "name": "squareMeters", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "hasPrivateBathroom": { + "name": "hasPrivateBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateKitchen": { + "name": "hasPrivateKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasPrivateToilet": { + "name": "hasPrivateToilet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requiresMedicalDocs": { + "name": "requiresMedicalDocs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "SpotStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'AVAILABLE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "PlacementSpot_housingUnitId_code_key": { + "name": "PlacementSpot_housingUnitId_code_key", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_housingUnitId_idx": { + "name": "PlacementSpot_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_requiresMedicalDocs_idx": { + "name": "PlacementSpot_requiresMedicalDocs_idx", + "columns": [ + { + "expression": "requiresMedicalDocs", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "PlacementSpot_type_status_idx": { + "name": "PlacementSpot_type_status_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "PlacementSpot_housingUnitId_fkey": { + "name": "PlacementSpot_housingUnitId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "PlacementSpot_parentSpotId_fkey": { + "name": "PlacementSpot_parentSpotId_fkey", + "tableFrom": "PlacementSpot", + "tableTo": "PlacementSpot", + "columnsFrom": ["parentSpotId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Proposal": { + "name": "Proposal", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "ProposalType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "RuleCategory", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "targetRuleId": { + "name": "targetRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parentOrgRuleId": { + "name": "parentOrgRuleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByResidentId": { + "name": "proposedByResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proposedByStaff": { + "name": "proposedByStaff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ProposalStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DISCUSSION'" + }, + "decisionMode": { + "name": "decisionMode", + "type": "DecisionMode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "VoteThreshold", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "quorumPercent": { + "name": "quorumPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "approvalPercent": { + "name": "approvalPercent", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "eligibleVoterCount": { + "name": "eligibleVoterCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "discussionEndsAt": { + "name": "discussionEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingOpenedAt": { + "name": "votingOpenedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "votingEndsAt": { + "name": "votingEndsAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "decidedAt": { + "name": "decidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "outcomeSummary": { + "name": "outcomeSummary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecision": { + "name": "staffDecision", + "type": "StaffDecision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffUserId": { + "name": "staffUserId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staffDecidedAt": { + "name": "staffDecidedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Proposal_housingUnitId_status_idx": { + "name": "Proposal_housingUnitId_status_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Proposal_status_votingEndsAt_idx": { + "name": "Proposal_status_votingEndsAt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "votingEndsAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Proposal_housingUnitId_fkey": { + "name": "Proposal_housingUnitId_fkey", + "tableFrom": "Proposal", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_targetRuleId_fkey": { + "name": "Proposal_targetRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": ["targetRuleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Proposal_parentOrgRuleId_fkey": { + "name": "Proposal_parentOrgRuleId_fkey", + "tableFrom": "Proposal", + "tableTo": "HouseRule", + "columnsFrom": ["parentOrgRuleId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Proposal_proposedByResidentId_fkey": { + "name": "Proposal_proposedByResidentId_fkey", + "tableFrom": "Proposal", + "tableTo": "Resident", + "columnsFrom": ["proposedByResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Resident": { + "name": "Resident", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ageRange": { + "name": "ageRange", + "type": "AgeRange", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "Gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "familyStatus": { + "name": "familyStatus", + "type": "FamilyStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sleepSchedule": { + "name": "sleepSchedule", + "type": "SleepSchedule", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "noiseTolerance": { + "name": "noiseTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cleanlinessPractice": { + "name": "cleanlinessPractice", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "guestTolerance": { + "name": "guestTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "socialStyle": { + "name": "socialStyle", + "type": "SocialStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "culturalRegion": { + "name": "culturalRegion", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conflictStyle": { + "name": "conflictStyle", + "type": "ConflictStyle", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'COOPERATIVE'" + }, + "smokingStatus": { + "name": "smokingStatus", + "type": "SmokingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dietaryNeeds": { + "name": "dietaryNeeds", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "mobilityNeeds": { + "name": "mobilityNeeds", + "type": "MobilityNeed", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "medicalEquipment": { + "name": "medicalEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "petTolerance": { + "name": "petTolerance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedBathroom": { + "name": "sharedBathroom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sharedKitchen": { + "name": "sharedKitchen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "privacyNeed": { + "name": "privacyNeed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "choresContribution": { + "name": "choresContribution", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "recyclingKnowledge": { + "name": "recyclingKnowledge", + "type": "RecyclingKnowledge", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "roomSharingStatus": { + "name": "roomSharingStatus", + "type": "RoomSharingStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CAN_SHARE'" + }, + "hasNightDisturbances": { + "name": "hasNightDisturbances", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needsQuietEnvironment": { + "name": "needsQuietEnvironment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hasSleepEquipment": { + "name": "hasSleepEquipment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "supportLevel": { + "name": "supportLevel", + "type": "SupportLevel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'STANDARD'" + }, + "interpreterNeed": { + "name": "interpreterNeed", + "type": "InterpreterNeed", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NONE'" + }, + "roommatePreferences": { + "name": "roommatePreferences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "ResidentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasMedicalDocumentation": { + "name": "hasMedicalDocumentation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "medicalDocType": { + "name": "medicalDocType", + "type": "MedicalDocType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "medicalDocDate": { + "name": "medicalDocDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "medicalDocNotes": { + "name": "medicalDocNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferencesCompletedAt": { + "name": "preferencesCompletedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "cleanlinessExpectation": { + "name": "cleanlinessExpectation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "chaosTolerance": { + "name": "chaosTolerance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profileVisibility": { + "name": "profileVisibility", + "type": "ProfileVisibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ROOMMATES'" + }, + "livingSkillsSupport": { + "name": "livingSkillsSupport", + "type": "LivingSkillsSupport", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'INDEPENDENT'" + } + }, + "indexes": { + "Resident_ageRange_gender_idx": { + "name": "Resident_ageRange_gender_idx", + "columns": [ + { + "expression": "ageRange", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gender", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_code_key": { + "name": "Resident_code_key", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_livingSkillsSupport_idx": { + "name": "Resident_livingSkillsSupport_idx", + "columns": [ + { + "expression": "livingSkillsSupport", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_status_idx": { + "name": "Resident_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Resident_interpreterNeed_idx": { + "name": "Resident_interpreterNeed_idx", + "columns": [ + { + "expression": "interpreterNeed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocument": { + "name": "ResidentDocument", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sizeBytes": { + "name": "sizeBytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploadedByUserId": { + "name": "uploadedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ResidentDocument_residentId_createdAt_idx": { + "name": "ResidentDocument_residentId_createdAt_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ResidentDocument_residentId_fkey": { + "name": "ResidentDocument_residentId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ResidentDocument_uploadedByUserId_fkey": { + "name": "ResidentDocument_uploadedByUserId_fkey", + "tableFrom": "ResidentDocument", + "tableTo": "User", + "columnsFrom": ["uploadedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentDocumentBlob": { + "name": "ResidentDocumentBlob", + "schema": "", + "columns": { + "documentId": { + "name": "documentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentDocumentBlob_documentId_fkey": { + "name": "ResidentDocumentBlob_documentId_fkey", + "tableFrom": "ResidentDocumentBlob", + "tableTo": "ResidentDocument", + "columnsFrom": ["documentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ResidentPhoto": { + "name": "ResidentPhoto", + "schema": "", + "columns": { + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ResidentPhoto_residentId_fkey": { + "name": "ResidentPhoto_residentId_fkey", + "tableFrom": "ResidentPhoto", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RuleAcknowledgement": { + "name": "RuleAcknowledgement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ruleId": { + "name": "ruleId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ruleVersion": { + "name": "ruleVersion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "acknowledgedAt": { + "name": "acknowledgedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "RuleAcknowledgement_residentId_idx": { + "name": "RuleAcknowledgement_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "RuleAcknowledgement_ruleId_residentId_ruleVersion_key": { + "name": "RuleAcknowledgement_ruleId_residentId_ruleVersion_key", + "columns": [ + { + "expression": "ruleId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ruleVersion", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RuleAcknowledgement_ruleId_fkey": { + "name": "RuleAcknowledgement_ruleId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "HouseRule", + "columnsFrom": ["ruleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RuleAcknowledgement_residentId_fkey": { + "name": "RuleAcknowledgement_residentId_fkey", + "tableFrom": "RuleAcknowledgement", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SatisfactionCheckIn": { + "name": "SatisfactionCheckIn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "placementId": { + "name": "placementId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkInType": { + "name": "checkInType", + "type": "CheckInType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "weekNumber": { + "name": "weekNumber", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "overallSatisfaction": { + "name": "overallSatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roommateRelations": { + "name": "roommateRelations", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "facilitySatisfaction": { + "name": "facilitySatisfaction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "safetyFeeling": { + "name": "safetyFeeling", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "concerns": { + "name": "concerns", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "improvements": { + "name": "improvements", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "positives": { + "name": "positives", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedBy": { + "name": "collectedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isAnonymous": { + "name": "isAnonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appointmentId": { + "name": "appointmentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collectedByUserId": { + "name": "collectedByUserId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "SatisfactionCheckIn_appointmentId_key": { + "name": "SatisfactionCheckIn_appointmentId_key", + "columns": [ + { + "expression": "appointmentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_checkInType_idx": { + "name": "SatisfactionCheckIn_checkInType_idx", + "columns": [ + { + "expression": "checkInType", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_collectedByUserId_idx": { + "name": "SatisfactionCheckIn_collectedByUserId_idx", + "columns": [ + { + "expression": "collectedByUserId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "SatisfactionCheckIn_placementId_idx": { + "name": "SatisfactionCheckIn_placementId_idx", + "columns": [ + { + "expression": "placementId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "SatisfactionCheckIn_placementId_fkey": { + "name": "SatisfactionCheckIn_placementId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Placement", + "columnsFrom": ["placementId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_appointmentId_fkey": { + "name": "SatisfactionCheckIn_appointmentId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "Appointment", + "columnsFrom": ["appointmentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "SatisfactionCheckIn_collectedByUserId_fkey": { + "name": "SatisfactionCheckIn_collectedByUserId_fkey", + "tableFrom": "SatisfactionCheckIn", + "tableTo": "User", + "columnsFrom": ["collectedByUserId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Settlement": { + "name": "Settlement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromId": { + "name": "fromId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toId": { + "name": "toId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amountRappen": { + "name": "amountRappen", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "Settlement_housingUnitId_idx": { + "name": "Settlement_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Settlement_housingUnitId_fkey": { + "name": "Settlement_housingUnitId_fkey", + "tableFrom": "Settlement", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Settlement_fromId_fkey": { + "name": "Settlement_fromId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": ["fromId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "Settlement_toId_fkey": { + "name": "Settlement_toId_fkey", + "tableFrom": "Settlement", + "tableTo": "Resident", + "columnsFrom": ["toId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.StaffUnit": { + "name": "StaffUnit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "staffId": { + "name": "staffId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "housingUnitId": { + "name": "housingUnitId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "StaffUnit_staffId_housingUnitId_key": { + "name": "StaffUnit_staffId_housingUnitId_key", + "columns": [ + { + "expression": "staffId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "StaffUnit_housingUnitId_idx": { + "name": "StaffUnit_housingUnitId_idx", + "columns": [ + { + "expression": "housingUnitId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "StaffUnit_staffId_fkey": { + "name": "StaffUnit_staffId_fkey", + "tableFrom": "StaffUnit", + "tableTo": "User", + "columnsFrom": ["staffId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "StaffUnit_housingUnitId_fkey": { + "name": "StaffUnit_housingUnitId_fkey", + "tableFrom": "StaffUnit", + "tableTo": "HousingUnit", + "columnsFrom": ["housingUnitId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.SystemConfig": { + "name": "SystemConfig", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'singleton'" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "pilotBaselineIncidentsPerMonth": { + "name": "pilotBaselineIncidentsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineRelocationsPerMonth": { + "name": "pilotBaselineRelocationsPerMonth", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotBaselineMediationHoursPerWeek": { + "name": "pilotBaselineMediationHoursPerWeek", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "pilotStartDate": { + "name": "pilotStartDate", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskAttentionFlag": { + "name": "TaskAttentionFlag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flaggedById": { + "name": "flaggedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isResolved": { + "name": "isResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "resolvedAt": { + "name": "resolvedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "resolvedByCompletionId": { + "name": "resolvedByCompletionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskAttentionFlag_taskId_idx": { + "name": "TaskAttentionFlag_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskAttentionFlag_taskId_fkey": { + "name": "TaskAttentionFlag_taskId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_flaggedById_fkey": { + "name": "TaskAttentionFlag_flaggedById_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "Resident", + "columnsFrom": ["flaggedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskAttentionFlag_resolvedByCompletionId_fkey": { + "name": "TaskAttentionFlag_resolvedByCompletionId_fkey", + "tableFrom": "TaskAttentionFlag", + "tableTo": "TaskCompletion", + "columnsFrom": ["resolvedByCompletionId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskCompletion": { + "name": "TaskCompletion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedById": { + "name": "completedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "durationMinutes": { + "name": "durationMinutes", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completedItems": { + "name": "completedItems", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::TEXT[]" + } + }, + "indexes": { + "TaskCompletion_completedById_idx": { + "name": "TaskCompletion_completedById_idx", + "columns": [ + { + "expression": "completedById", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskCompletion_taskId_idx": { + "name": "TaskCompletion_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskCompletion_taskId_fkey": { + "name": "TaskCompletion_taskId_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskCompletion_completedById_fkey": { + "name": "TaskCompletion_completedById_fkey", + "tableFrom": "TaskCompletion", + "tableTo": "Resident", + "columnsFrom": ["completedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TaskRequest": { + "name": "TaskRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "taskId": { + "name": "taskId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedById": { + "name": "requestedById", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestedResidentId": { + "name": "requestedResidentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isBroadcast": { + "name": "isBroadcast", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "TaskRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "responseMessage": { + "name": "responseMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completionId": { + "name": "completionId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TaskRequest_requestedResidentId_idx": { + "name": "TaskRequest_requestedResidentId_idx", + "columns": [ + { + "expression": "requestedResidentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TaskRequest_taskId_idx": { + "name": "TaskRequest_taskId_idx", + "columns": [ + { + "expression": "taskId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TaskRequest_taskId_fkey": { + "name": "TaskRequest_taskId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "HouseholdTask", + "columnsFrom": ["taskId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedById_fkey": { + "name": "TaskRequest_requestedById_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": ["requestedById"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TaskRequest_requestedResidentId_fkey": { + "name": "TaskRequest_requestedResidentId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "Resident", + "columnsFrom": ["requestedResidentId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TaskRequest_completionId_fkey": { + "name": "TaskRequest_completionId_fkey", + "tableFrom": "TaskRequest", + "tableTo": "TaskCompletion", + "columnsFrom": ["completionId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TransferRequest": { + "name": "TransferRequest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currentPlacementId": { + "name": "currentPlacementId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "targetUnitId": { + "name": "targetUnitId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "TransferRequestStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "staffNotes": { + "name": "staffNotes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedBy": { + "name": "reviewedBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewedAt": { + "name": "reviewedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "TransferRequest_residentId_idx": { + "name": "TransferRequest_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "TransferRequest_status_idx": { + "name": "TransferRequest_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "TransferRequest_residentId_fkey": { + "name": "TransferRequest_residentId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "TransferRequest_currentPlacementId_fkey": { + "name": "TransferRequest_currentPlacementId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "Placement", + "columnsFrom": ["currentPlacementId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "TransferRequest_targetUnitId_fkey": { + "name": "TransferRequest_targetUnitId_fkey", + "tableFrom": "TransferRequest", + "tableTo": "HousingUnit", + "columnsFrom": ["targetUnitId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "StaffRole", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'BETREUUNG'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "lastLoginAt": { + "name": "lastLoginAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "StaffScope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OWN_DOMAIN'" + }, + "isSystemAdmin": { + "name": "isSystemAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "siteAccess": { + "name": "siteAccess", + "type": "SiteAccess", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ALL_UNITS'" + } + }, + "indexes": { + "User_code_idx": { + "name": "User_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_role_idx": { + "name": "User_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "User_scope_idx": { + "name": "User_scope_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_code_key": { + "name": "User_code_key", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Vote": { + "name": "Vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "proposalId": { + "name": "proposalId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "residentId": { + "name": "residentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "choice": { + "name": "choice", + "type": "VoteChoice", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "castAt": { + "name": "castAt", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "Vote_proposalId_residentId_key": { + "name": "Vote_proposalId_residentId_key", + "columns": [ + { + "expression": "proposalId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Vote_residentId_idx": { + "name": "Vote_residentId_idx", + "columns": [ + { + "expression": "residentId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Vote_proposalId_fkey": { + "name": "Vote_proposalId_fkey", + "tableFrom": "Vote", + "tableTo": "Proposal", + "columnsFrom": ["proposalId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Vote_residentId_fkey": { + "name": "Vote_residentId_fkey", + "tableFrom": "Vote", + "tableTo": "Resident", + "columnsFrom": ["residentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ActivityCategory": { + "name": "ActivityCategory", + "schema": "public", + "values": ["SPORT", "LANGUAGE", "CULTURE", "COMMUNITY", "FAMILY", "SUPPORT"] + }, + "public.ActivityCost": { + "name": "ActivityCost", + "schema": "public", + "values": ["FREE", "REDUCED", "PAID"] + }, + "public.ActivityStatus": { + "name": "ActivityStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] + }, + "public.AgeRange": { + "name": "AgeRange", + "schema": "public", + "values": ["YOUNG_ADULT", "ADULT", "MIDDLE_AGED", "SENIOR"] + }, + "public.AgreementStatus": { + "name": "AgreementStatus", + "schema": "public", + "values": ["PROPOSED", "ACCEPTED", "HELD", "BROKEN", "EXPIRED"] + }, + "public.ApplicationStage": { + "name": "ApplicationStage", + "schema": "public", + "values": ["INTERESTED", "APPLIED", "INTERVIEW", "ACCEPTED", "STARTED", "ENDED", "DECLINED"] + }, + "public.AppointmentStatus": { + "name": "AppointmentStatus", + "schema": "public", + "values": ["SCHEDULED", "COMPLETED", "CANCELLED", "NO_SHOW", "REQUESTED"] + }, + "public.AuthTokenPurpose": { + "name": "AuthTokenPurpose", + "schema": "public", + "values": ["VERIFY_EMAIL", "RESET_PASSWORD"] + }, + "public.CareRole": { + "name": "CareRole", + "schema": "public", + "values": ["HOUSING", "SOCIAL", "JOB", "VOLUNTEERING"] + }, + "public.CheckInType": { + "name": "CheckInType", + "schema": "public", + "values": ["INITIAL", "REGULAR", "AD_HOC", "EXIT"] + }, + "public.ComplaintStatus": { + "name": "ComplaintStatus", + "schema": "public", + "values": ["OPEN", "IN_REVIEW", "ANSWERED"] + }, + "public.ComplaintSubject": { + "name": "ComplaintSubject", + "schema": "public", + "values": ["STAFF", "ACCOMMODATION", "DECISION", "OTHER"] + }, + "public.ConflictStyle": { + "name": "ConflictStyle", + "schema": "public", + "values": ["AVOIDANT", "COOPERATIVE", "DIRECT"] + }, + "public.DecisionMode": { + "name": "DecisionMode", + "schema": "public", + "values": ["RESIDENT_BINDING", "RESIDENT_ADVISORY", "STAFF_ONLY"] + }, + "public.EndReason": { + "name": "EndReason", + "schema": "public", + "values": ["NATURAL", "CONFLICT", "REQUEST", "CAPACITY", "UPGRADE", "OTHER"] + }, + "public.EventRsvpStatus": { + "name": "EventRsvpStatus", + "schema": "public", + "values": ["GOING", "MAYBE", "DECLINED"] + }, + "public.FamilyStatus": { + "name": "FamilyStatus", + "schema": "public", + "values": ["SINGLE", "COUPLE", "FAMILY_WITH_CHILDREN", "SINGLE_PARENT"] + }, + "public.FollowUpPriority": { + "name": "FollowUpPriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.Gender": { + "name": "Gender", + "schema": "public", + "values": ["MALE", "FEMALE", "OTHER", "PREFER_NOT_SAY"] + }, + "public.HouseEventCategory": { + "name": "HouseEventCategory", + "schema": "public", + "values": ["HOUSE_MEETING", "SOCIAL", "CULTURE", "SUPPORT"] + }, + "public.HouseEventStatus": { + "name": "HouseEventStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "CANCELLED"] + }, + "public.HouseholdTaskCategory": { + "name": "HouseholdTaskCategory", + "schema": "public", + "values": ["CLEANING", "SHOPPING", "MAINTENANCE", "COOKING", "TRASH", "OTHER"] + }, + "public.HouseholdTaskPriority": { + "name": "HouseholdTaskPriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.HouseholdTaskStatus": { + "name": "HouseholdTaskStatus", + "schema": "public", + "values": ["IDLE", "NEEDS_ATTENTION", "REQUESTED", "IN_PROGRESS"] + }, + "public.HouseholdTaskType": { + "name": "HouseholdTaskType", + "schema": "public", + "values": ["ONE_TIME", "RECURRING_SCHEDULED", "RECURRING_AS_NEEDED"] + }, + "public.HousingStatus": { + "name": "HousingStatus", + "schema": "public", + "values": ["AVAILABLE", "FULL", "MAINTENANCE", "CLOSED"] + }, + "public.IncidentCategory": { + "name": "IncidentCategory", + "schema": "public", + "values": ["INTERPERSONAL", "MAINTENANCE", "SAFETY", "WELLBEING"] + }, + "public.IncidentSeverity": { + "name": "IncidentSeverity", + "schema": "public", + "values": ["LOW", "MEDIUM", "HIGH", "CRITICAL"] + }, + "public.IncidentType": { + "name": "IncidentType", + "schema": "public", + "values": [ + "NOISE_COMPLAINT", + "CLEANLINESS_DISPUTE", + "PERSONAL_CONFLICT", + "CULTURAL_FRICTION", + "SPACE_DISPUTE", + "SCHEDULE_CONFLICT", + "SAFETY_CONCERN", + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY_SYSTEM", + "GENERAL_MAINTENANCE", + "LOW_SATISFACTION", + "OTHER" + ] + }, + "public.InterpreterNeed": { + "name": "InterpreterNeed", + "schema": "public", + "values": ["NONE", "FOR_COMPLEX", "ALWAYS"] + }, + "public.InvolvementRole": { + "name": "InvolvementRole", + "schema": "public", + "values": ["INVOLVED", "WITNESS", "MEDIATOR"] + }, + "public.LearningKind": { + "name": "LearningKind", + "schema": "public", + "values": [ + "LANGUAGE_TEST", + "COURSE", + "INFORMAL", + "QUALIFICATION", + "VOLUNTEERING", + "COMMUNITY_SERVICE", + "EMPLOYMENT", + "INTERNSHIP" + ] + }, + "public.LearningStatus": { + "name": "LearningStatus", + "schema": "public", + "values": ["PLANNED", "IN_PROGRESS", "COMPLETED", "EXPIRED"] + }, + "public.LivingSkillsSupport": { + "name": "LivingSkillsSupport", + "schema": "public", + "values": ["INDEPENDENT", "SOME_SUPPORT", "REGULAR_SUPPORT"] + }, + "public.MaintenanceCategory": { + "name": "MaintenanceCategory", + "schema": "public", + "values": [ + "PLUMBING", + "ELECTRICAL", + "HEATING_COOLING", + "APPLIANCE", + "STRUCTURAL", + "PEST_CONTROL", + "SECURITY", + "CLEANING", + "EXTERIOR", + "OTHER" + ] + }, + "public.MaintenancePriority": { + "name": "MaintenancePriority", + "schema": "public", + "values": ["LOW", "NORMAL", "HIGH", "URGENT"] + }, + "public.MaintenanceStatus": { + "name": "MaintenanceStatus", + "schema": "public", + "values": ["OPEN", "ASSIGNED", "IN_PROGRESS", "ON_HOLD", "COMPLETED", "CANCELLED"] + }, + "public.MarketplacePostKind": { + "name": "MarketplacePostKind", + "schema": "public", + "values": ["GIVE_AWAY", "LEND", "WANTED", "OFFER_HELP", "NEED_HELP"] + }, + "public.MarketplacePostStatus": { + "name": "MarketplacePostStatus", + "schema": "public", + "values": ["OPEN", "CLAIMED", "CLOSED"] + }, + "public.MedicalDocType": { + "name": "MedicalDocType", + "schema": "public", + "values": ["PRIVATE_ROOM", "STUDIO", "BOTH"] + }, + "public.MobilityNeed": { + "name": "MobilityNeed", + "schema": "public", + "values": ["NONE", "GROUND_FLOOR", "WHEELCHAIR"] + }, + "public.OpportunityKind": { + "name": "OpportunityKind", + "schema": "public", + "values": ["VOLUNTEERING", "COMMUNITY_SERVICE", "EMPLOYMENT", "INTERNSHIP"] + }, + "public.OpportunityStatus": { + "name": "OpportunityStatus", + "schema": "public", + "values": ["DRAFT", "PUBLISHED", "ARCHIVED"] + }, + "public.PermitRequirement": { + "name": "PermitRequirement", + "schema": "public", + "values": ["NONE", "EMPLOYER_NOTIFIES", "PERMIT_REQUIRED"] + }, + "public.PlacementStatus": { + "name": "PlacementStatus", + "schema": "public", + "values": ["ACTIVE", "ENDED", "TRANSFERRED"] + }, + "public.ProfileVisibility": { + "name": "ProfileVisibility", + "schema": "public", + "values": ["PRIVATE", "ROOMMATES", "RESIDENTS"] + }, + "public.ProposalStatus": { + "name": "ProposalStatus", + "schema": "public", + "values": [ + "DISCUSSION", + "VOTING", + "NEEDS_STAFF_CONFIRMATION", + "ACCEPTED", + "REJECTED", + "WITHDRAWN", + "VETOED", + "EXPIRED" + ] + }, + "public.ProposalType": { + "name": "ProposalType", + "schema": "public", + "values": ["ADD_RULE", "AMEND_RULE", "REPEAL_RULE", "HOUSE_DECISION"] + }, + "public.RecyclingKnowledge": { + "name": "RecyclingKnowledge", + "schema": "public", + "values": ["NONE", "BASIC", "GOOD"] + }, + "public.ResidentOrStaff": { + "name": "ResidentOrStaff", + "schema": "public", + "values": ["RESIDENT", "STAFF"] + }, + "public.ResidentStatus": { + "name": "ResidentStatus", + "schema": "public", + "values": ["ACTIVE", "PLACED", "TRANSFERRED", "EXITED"] + }, + "public.ResolutionStage": { + "name": "ResolutionStage", + "schema": "public", + "values": [ + "REPORTED", + "SELF_RESOLUTION", + "PEER_MEDIATION", + "STAFF_MEDIATION", + "FORMAL_MEASURE", + "CLOSED" + ] + }, + "public.RoomSharingStatus": { + "name": "RoomSharingStatus", + "schema": "public", + "values": ["CAN_SHARE", "PREFERS_PRIVATE", "NEEDS_PRIVATE"] + }, + "public.RuleCategory": { + "name": "RuleCategory", + "schema": "public", + "values": [ + "SAFETY", + "RESPECT", + "NOISE", + "CLEANLINESS", + "KITCHEN", + "BATHROOM", + "GUESTS", + "SHARED_SPACES", + "COSTS", + "COMMUNICATION", + "OTHER" + ] + }, + "public.RuleDelegation": { + "name": "RuleDelegation", + "schema": "public", + "values": ["FIXED", "UNIT_MAY_STRENGTHEN", "UNIT_DECIDES"] + }, + "public.RuleScope": { + "name": "RuleScope", + "schema": "public", + "values": ["ORG", "UNIT"] + }, + "public.RuleStatus": { + "name": "RuleStatus", + "schema": "public", + "values": ["ACTIVE", "SUPERSEDED", "ARCHIVED"] + }, + "public.SiteAccess": { + "name": "SiteAccess", + "schema": "public", + "values": ["ALL_UNITS", "ASSIGNED_UNITS"] + }, + "public.SleepSchedule": { + "name": "SleepSchedule", + "schema": "public", + "values": ["EARLY_BIRD", "STANDARD", "NIGHT_OWL", "IRREGULAR"] + }, + "public.SmokingStatus": { + "name": "SmokingStatus", + "schema": "public", + "values": ["NON_SMOKER", "OUTDOOR_SMOKER", "INDOOR_SMOKER"] + }, + "public.SocialStyle": { + "name": "SocialStyle", + "schema": "public", + "values": ["INTROVERTED", "MODERATE", "EXTROVERTED"] + }, + "public.SpotStatus": { + "name": "SpotStatus", + "schema": "public", + "values": ["AVAILABLE", "OCCUPIED", "MAINTENANCE", "CLOSED"] + }, + "public.SpotType": { + "name": "SpotType", + "schema": "public", + "values": ["BED", "PRIVATE_ROOM", "STUDIO", "ROOM"] + }, + "public.StaffDecision": { + "name": "StaffDecision", + "schema": "public", + "values": ["CONFIRMED", "VETOED"] + }, + "public.StaffRole": { + "name": "StaffRole", + "schema": "public", + "values": ["ADMIN", "BETREUUNG", "SOZIALARBEIT", "JOBCOACH", "FREIWILLIGENARBEIT"] + }, + "public.StaffScope": { + "name": "StaffScope", + "schema": "public", + "values": ["OWN_DOMAIN", "ALL_DOMAINS"] + }, + "public.SupportLevel": { + "name": "SupportLevel", + "schema": "public", + "values": ["STANDARD", "ELEVATED", "INTENSIVE"] + }, + "public.TaskRequestStatus": { + "name": "TaskRequestStatus", + "schema": "public", + "values": ["PENDING", "ACCEPTED", "DECLINED", "COMPLETED"] + }, + "public.TransferRequestStatus": { + "name": "TransferRequestStatus", + "schema": "public", + "values": ["PENDING", "APPROVED", "DENIED", "COMPLETED", "CANCELLED"] + }, + "public.VoteChoice": { + "name": "VoteChoice", + "schema": "public", + "values": ["YES", "NO", "ABSTAIN", "BLOCK"] + }, + "public.VoteThreshold": { + "name": "VoteThreshold", + "schema": "public", + "values": ["CONSENSUS", "SUPERMAJORITY", "SIMPLE_MAJORITY"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1c1874bb..2f2ad9c9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1788340236588, "tag": "0001_staff_site_access", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1788341777738, + "tag": "0002_interpreter_need", + "breakpoints": true } ] } diff --git a/src/components/residents/CareWorkspace.tsx b/src/components/residents/CareWorkspace.tsx index d64847c7..a857fa07 100644 --- a/src/components/residents/CareWorkspace.tsx +++ b/src/components/residents/CareWorkspace.tsx @@ -8,7 +8,7 @@ import { CARE_ROLES, type CareRoleId, } from '@/lib/config/care' -import type { InterpreterNeed } from '@prisma/client' +import type { InterpreterNeed } from '@/lib/db' import { INTERPRETER_LABELS, needsInterpreter } from '@/lib/config/interpreting' import type { CareAppointment, CareAttributeValue } from '@/lib/actions/care' import { diff --git a/src/components/residents/__tests__/CareWorkspace.test.tsx b/src/components/residents/__tests__/CareWorkspace.test.tsx index f059944a..ccaed64f 100644 --- a/src/components/residents/__tests__/CareWorkspace.test.tsx +++ b/src/components/residents/__tests__/CareWorkspace.test.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' -import type { InterpreterNeed } from '@prisma/client' +import type { InterpreterNeed } from '@/lib/db' import { CareWorkspace } from '../CareWorkspace' import { CARE_ROLES, CARE_ROLE_LABELS, writableCareDomains } from '@/lib/config/care' import { ASSIGNABLE_STAFF_ROLES, type StaffRole } from '@/lib/auth/role-policy' diff --git a/src/lib/config/interpreting.ts b/src/lib/config/interpreting.ts index 1c8f6126..9d1e7698 100644 --- a/src/lib/config/interpreting.ts +++ b/src/lib/config/interpreting.ts @@ -1,4 +1,4 @@ -import type { InterpreterNeed } from '@prisma/client' +import type { InterpreterNeed } from '@/lib/db' /** * Interpreting — the need, and the lead time that makes it operational. diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index e60f5f89..bc4b5932 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -173,6 +173,7 @@ export const incidentType = pgEnum('IncidentType', [ 'LOW_SATISFACTION', 'OTHER', ]) +export const interpreterNeed = pgEnum('InterpreterNeed', ['NONE', 'FOR_COMPLEX', 'ALWAYS']) export const involvementRole = pgEnum('InvolvementRole', ['INVOLVED', 'WITNESS', 'MEDIATOR']) export const learningKind = pgEnum('LearningKind', [ 'LANGUAGE_TEST', @@ -1081,6 +1082,7 @@ export const resident = pgTable( needsQuietEnvironment: boolean().default(false).notNull(), hasSleepEquipment: boolean().default(false).notNull(), supportLevel: supportLevel().default('STANDARD').notNull(), + interpreterNeed: interpreterNeed().default('NONE').notNull(), roommatePreferences: text(), status: residentStatus().default('ACTIVE').notNull(), notes: text(), @@ -1108,6 +1110,7 @@ export const resident = pgTable( table.livingSkillsSupport.asc().nullsLast(), ), index('Resident_status_idx').using('btree', table.status.asc().nullsLast()), + index('Resident_interpreterNeed_idx').using('btree', table.interpreterNeed.asc().nullsLast()), ], ) diff --git a/src/lib/db/types.ts b/src/lib/db/types.ts index 2626989d..ca294034 100644 --- a/src/lib/db/types.ts +++ b/src/lib/db/types.ts @@ -691,3 +691,9 @@ export const SiteAccess = Object.freeze({ ASSIGNED_UNITS: 'ASSIGNED_UNITS', } as const) satisfies Record export type SiteAccess = (typeof SiteAccess)[keyof typeof SiteAccess] +export const InterpreterNeed = Object.freeze({ + NONE: 'NONE', + FOR_COMPLEX: 'FOR_COMPLEX', + ALWAYS: 'ALWAYS', +} as const) satisfies Record +export type InterpreterNeed = (typeof InterpreterNeed)[keyof typeof InterpreterNeed]