diff --git a/drizzle/0027_pets_module.sql b/drizzle/0027_pets_module.sql new file mode 100644 index 0000000..6647d8e --- /dev/null +++ b/drizzle/0027_pets_module.sql @@ -0,0 +1,70 @@ +-- Custom SQL migration file, put your code below! -- +CREATE TABLE "pets" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "household_id" uuid NOT NULL, + "name" text NOT NULL, + "species" text NOT NULL, + "breed" text, + "birth_date" date, + "notes" text, + "primary_image_url" text, + "images" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pets_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action +); +--> statement-breakpoint +CREATE TABLE "pet_appointments" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "pet_id" uuid NOT NULL, + "household_id" uuid NOT NULL, + "title" text NOT NULL, + "appointment_at" timestamp with time zone NOT NULL, + "clinic" text, + "notes" text, + "calendar_event_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pet_appointments_pet_id_pets_id_fk" FOREIGN KEY ("pet_id") REFERENCES "public"."pets"("id") ON DELETE cascade ON UPDATE no action, + CONSTRAINT "pet_appointments_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action +); +--> statement-breakpoint +CREATE TABLE "pet_vaccinations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "pet_id" uuid NOT NULL, + "household_id" uuid NOT NULL, + "name" text NOT NULL, + "administered_on" date NOT NULL, + "due_on" date, + "provider" text, + "notes" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pet_vaccinations_pet_id_pets_id_fk" FOREIGN KEY ("pet_id") REFERENCES "public"."pets"("id") ON DELETE cascade ON UPDATE no action, + CONSTRAINT "pet_vaccinations_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action +); +--> statement-breakpoint +CREATE TABLE "pet_prescriptions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "pet_id" uuid NOT NULL, + "household_id" uuid NOT NULL, + "medication" text NOT NULL, + "dosage" text NOT NULL, + "instructions" text, + "prescribed_on" date, + "expires_on" date, + "refills_remaining" integer, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "pet_prescriptions_pet_id_pets_id_fk" FOREIGN KEY ("pet_id") REFERENCES "public"."pets"("id") ON DELETE cascade ON UPDATE no action, + CONSTRAINT "pet_prescriptions_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action +); +--> statement-breakpoint +CREATE INDEX "pets_household_idx" ON "pets" USING btree ("household_id"); +--> statement-breakpoint +CREATE INDEX "pet_appointments_pet_at_idx" ON "pet_appointments" USING btree ("pet_id", "appointment_at"); +--> statement-breakpoint +CREATE INDEX "pet_vaccinations_pet_due_idx" ON "pet_vaccinations" USING btree ("pet_id", "due_on"); +--> statement-breakpoint +CREATE INDEX "pet_prescriptions_pet_active_idx" ON "pet_prescriptions" USING btree ("pet_id", "active"); diff --git a/drizzle/meta/0027_snapshot.json b/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..cb2f728 --- /dev/null +++ b/drizzle/meta/0027_snapshot.json @@ -0,0 +1,1397 @@ +{ + "id": "0191c7dd-700b-4c05-9cdb-fa883d6ed838", + "prevId": "a7c89adf-0d22-49a9-81cb-bbf3d4472251", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "accounts_provider_provider_account_id_pk": { + "name": "accounts_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_household_created_idx": { + "name": "activity_log_household_created_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "activity_log_household_id_households_id_fk": { + "name": "activity_log_household_id_households_id_fk", + "tableFrom": "activity_log", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "activity_log_actor_id_users_id_fk": { + "name": "activity_log_actor_id_users_id_fk", + "tableFrom": "activity_log", + "columnsFrom": [ + "actor_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_members": { + "name": "household_members", + "schema": "", + "columns": { + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "household_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": { + "household_members_household_id_households_id_fk": { + "name": "household_members_household_id_households_id_fk", + "tableFrom": "household_members", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "household_members_user_id_users_id_fk": { + "name": "household_members_user_id_users_id_fk", + "tableFrom": "household_members", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "household_members_household_id_user_id_pk": { + "name": "household_members_household_id_user_id_pk", + "columns": [ + "household_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.households": { + "name": "households", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reminders": { + "name": "reminders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fire_at": { + "name": "fire_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'in_app'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reminders_entity_unique": { + "name": "reminders_entity_unique", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "reminders_household_fire_at_idx": { + "name": "reminders_household_fire_at_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fire_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "reminders_household_id_households_id_fk": { + "name": "reminders_household_id_households_id_fk", + "tableFrom": "reminders", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "theme_mode": { + "name": "theme_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "default_dashboard_layout": { + "name": "default_dashboard_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification_tokens": { + "name": "verification_tokens", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_tokens_identifier_token_pk": { + "name": "verification_tokens_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_events": { + "name": "calendar_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "calendar_id": { + "name": "calendar_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_source": { + "name": "external_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendar_events_calendar_start_idx": { + "name": "calendar_events_calendar_start_idx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "calendar_events_calendar_id_calendars_id_fk": { + "name": "calendar_events_calendar_id_calendars_id_fk", + "tableFrom": "calendar_events", + "columnsFrom": [ + "calendar_id" + ], + "tableTo": "calendars", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "calendar_events_owner_id_users_id_fk": { + "name": "calendar_events_owner_id_users_id_fk", + "tableFrom": "calendar_events", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "calendar_events_range_check": { + "name": "calendar_events_range_check", + "value": "\"calendar_events\".\"end_at\" >= \"calendar_events\".\"start_at\"" + } + }, + "isRLSEnabled": false + }, + "public.calendars": { + "name": "calendars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'household'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "calendars_household_idx": { + "name": "calendars_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "calendars_owner_idx": { + "name": "calendars_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "calendars_household_id_households_id_fk": { + "name": "calendars_household_id_households_id_fk", + "tableFrom": "calendars", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "calendars_owner_id_users_id_fk": { + "name": "calendars_owner_id_users_id_fk", + "tableFrom": "calendars", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "calendars_visibility_check": { + "name": "calendars_visibility_check", + "value": "\"calendars\".\"visibility\" in ('private', 'household')" + } + }, + "isRLSEnabled": false + }, + "public.list_items": { + "name": "list_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "done": { + "name": "done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qty": { + "name": "qty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "list_items_list_position_idx": { + "name": "list_items_list_position_idx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "list_items_assignee_idx": { + "name": "list_items_assignee_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "list_items_list_id_lists_id_fk": { + "name": "list_items_list_id_lists_id_fk", + "tableFrom": "list_items", + "columnsFrom": [ + "list_id" + ], + "tableTo": "lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "list_items_assignee_id_users_id_fk": { + "name": "list_items_assignee_id_users_id_fk", + "tableFrom": "list_items", + "columnsFrom": [ + "assignee_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lists_household_type_idx": { + "name": "lists_household_type_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "lists_household_archived_idx": { + "name": "lists_household_archived_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "lists_household_id_households_id_fk": { + "name": "lists_household_id_households_id_fk", + "tableFrom": "lists", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notes": { + "name": "notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "remind_at": { + "name": "remind_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notes_household_pinned_updated_idx": { + "name": "notes_household_pinned_updated_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pinned", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "notes_author_idx": { + "name": "notes_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "notes_household_id_households_id_fk": { + "name": "notes_household_id_households_id_fk", + "tableFrom": "notes", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notes_author_id_users_id_fk": { + "name": "notes_author_id_users_id_fk", + "tableFrom": "notes", + "columnsFrom": [ + "author_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bang_events": { + "name": "bang_events", + "schema": "public", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recorded_by": { + "name": "recorded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_on": { + "name": "occurred_on", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bang_events_household_occurred_idx": { + "name": "bang_events_household_occurred_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_on", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "bang_events_household_id_households_id_fk": { + "name": "bang_events_household_id_households_id_fk", + "tableFrom": "bang_events", + "columnsFrom": [ + "household_id" + ], + "tableTo": "households", + "schemaTo": "public", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "bang_events_recorded_by_users_id_fk": { + "name": "bang_events_recorded_by_users_id_fk", + "tableFrom": "bang_events", + "columnsFrom": [ + "recorded_by" + ], + "tableTo": "users", + "schemaTo": "public", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.household_member_role": { + "name": "household_member_role", + "schema": "public", + "values": [ + "owner", + "member" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 0eb35e2..9117ef2 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1783561000000, "tag": "0026_assistant_model_route", "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1783648174004, + "tag": "0027_pets_module", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/modules/pets/schema.ts b/src/modules/pets/schema.ts new file mode 100644 index 0000000..ae60508 --- /dev/null +++ b/src/modules/pets/schema.ts @@ -0,0 +1,102 @@ +import { + boolean, + date, + index, + integer, + jsonb, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; +import { households } from "../_core/schema"; + +export const pets = pgTable( + "pets", + { + id: uuid("id").primaryKey().defaultRandom(), + householdId: uuid("household_id") + .notNull() + .references(() => households.id, { onDelete: "cascade" }), + name: text("name").notNull(), + species: text("species").notNull(), + breed: text("breed"), + birthDate: date("birth_date"), + notes: text("notes"), + primaryImageUrl: text("primary_image_url"), + images: jsonb("images").notNull().default([]).$type(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("pets_household_idx").on(table.householdId)], +); + +export const petAppointments = pgTable( + "pet_appointments", + { + id: uuid("id").primaryKey().defaultRandom(), + petId: uuid("pet_id") + .notNull() + .references(() => pets.id, { onDelete: "cascade" }), + householdId: uuid("household_id") + .notNull() + .references(() => households.id, { onDelete: "cascade" }), + title: text("title").notNull(), + appointmentAt: timestamp("appointment_at", { withTimezone: true }).notNull(), + clinic: text("clinic"), + notes: text("notes"), + calendarEventId: uuid("calendar_event_id"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("pet_appointments_pet_at_idx").on(table.petId, table.appointmentAt)], +); + +export const petVaccinations = pgTable( + "pet_vaccinations", + { + id: uuid("id").primaryKey().defaultRandom(), + petId: uuid("pet_id") + .notNull() + .references(() => pets.id, { onDelete: "cascade" }), + householdId: uuid("household_id") + .notNull() + .references(() => households.id, { onDelete: "cascade" }), + name: text("name").notNull(), + administeredOn: date("administered_on").notNull(), + dueOn: date("due_on"), + provider: text("provider"), + notes: text("notes"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("pet_vaccinations_pet_due_idx").on(table.petId, table.dueOn)], +); + +export const petPrescriptions = pgTable( + "pet_prescriptions", + { + id: uuid("id").primaryKey().defaultRandom(), + petId: uuid("pet_id") + .notNull() + .references(() => pets.id, { onDelete: "cascade" }), + householdId: uuid("household_id") + .notNull() + .references(() => households.id, { onDelete: "cascade" }), + medication: text("medication").notNull(), + dosage: text("dosage").notNull(), + instructions: text("instructions"), + prescribedOn: date("prescribed_on"), + expiresOn: date("expires_on"), + refillsRemaining: integer("refills_remaining"), + active: boolean("active").notNull().default(true), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("pet_prescriptions_pet_active_idx").on(table.petId, table.active)], +); + +export type Pet = typeof pets.$inferSelect; +export type PetAppointment = typeof petAppointments.$inferSelect; +export type PetVaccination = typeof petVaccinations.$inferSelect; +export type PetPrescription = typeof petPrescriptions.$inferSelect; diff --git a/src/modules/pets/server/schemas.ts b/src/modules/pets/server/schemas.ts new file mode 100644 index 0000000..59c0247 --- /dev/null +++ b/src/modules/pets/server/schemas.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); +const optionalText = (max: number) => z.string().trim().max(max).nullable().optional(); + +export const petInput = z.object({ + name: z.string().trim().min(1).max(120), + species: z.string().trim().min(1).max(80), + breed: optionalText(120), + birthDate: isoDate.nullable().optional(), + notes: optionalText(4000), + images: z.array(z.string().min(1)).max(10).default([]), + primaryImageUrl: z.string().min(1).nullable().optional(), +}); + +export const petUpdateInput = petInput.partial(); + +export const appointmentInput = z.object({ + petId: z.string().uuid(), + title: z.string().trim().min(1).max(160), + appointmentAt: z.coerce.date(), + clinic: optionalText(160), + notes: optionalText(4000), + reminderOffsets: z.array(z.number().int().min(0).max(43_200)).default([30]), +}); + +export const appointmentUpdateInput = appointmentInput.omit({ petId: true }).partial(); + +export const vaccinationInput = z.object({ + petId: z.string().uuid(), + name: z.string().trim().min(1).max(160), + administeredOn: isoDate, + dueOn: isoDate.nullable().optional(), + provider: optionalText(160), + notes: optionalText(4000), +}); + +export const vaccinationUpdateInput = vaccinationInput.omit({ petId: true }).partial(); + +export const prescriptionInput = z.object({ + petId: z.string().uuid(), + medication: z.string().trim().min(1).max(160), + dosage: z.string().trim().min(1).max(160), + instructions: optionalText(2000), + prescribedOn: isoDate.nullable().optional(), + expiresOn: isoDate.nullable().optional(), + refillsRemaining: z.number().int().min(0).max(99).nullable().optional(), + active: z.boolean().default(true), +}); + +export const prescriptionUpdateInput = prescriptionInput.omit({ petId: true }).partial(); + +export const scheduleAppointmentInput = z.object({ + appointmentId: z.string().uuid(), + calendarId: z.string().uuid(), +}); diff --git a/tests/unit/pets-schemas.test.ts b/tests/unit/pets-schemas.test.ts new file mode 100644 index 0000000..bf7a806 --- /dev/null +++ b/tests/unit/pets-schemas.test.ts @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + appointmentInput, + petInput, + prescriptionInput, + scheduleAppointmentInput, + vaccinationInput, +} from "../../src/modules/pets/server/schemas"; + +const PET_ID = "f5a50e32-a217-4d65-ae10-98a1f441d171"; +const CALENDAR_ID = "bd8e10c2-58f4-4db8-9326-0b50fdda47bb"; + +test("pet and medical record schemas accept bounded valid input", () => { + assert.equal(petInput.parse({ name: "Mochi", species: "cat", images: [] }).name, "Mochi"); + assert.equal( + appointmentInput.parse({ + petId: PET_ID, + title: "Annual exam", + appointmentAt: "2026-08-01T15:00:00.000Z", + }).title, + "Annual exam", + ); + assert.equal( + vaccinationInput.parse({ petId: PET_ID, name: "Rabies", administeredOn: "2026-07-01" }).name, + "Rabies", + ); + assert.equal( + prescriptionInput.parse({ petId: PET_ID, medication: "Apoquel", dosage: "5 mg daily" }).active, + true, + ); + assert.deepEqual( + scheduleAppointmentInput.parse({ appointmentId: PET_ID, calendarId: CALENDAR_ID }), + { + appointmentId: PET_ID, + calendarId: CALENDAR_ID, + }, + ); +}); + +test("pet and medical record schemas reject invalid identifiers, dates, and galleries", () => { + assert.throws(() => petInput.parse({ name: "", species: "cat" })); + assert.throws(() => + petInput.parse({ + name: "Mochi", + species: "cat", + images: Array(11).fill("/api/uploads/pets/a.jpg"), + }), + ); + assert.throws(() => + appointmentInput.parse({ petId: "not-a-uuid", title: "Exam", appointmentAt: "not-a-date" }), + ); + assert.throws(() => + vaccinationInput.parse({ petId: PET_ID, name: "Rabies", administeredOn: "07/01/2026" }), + ); + assert.throws(() => + scheduleAppointmentInput.parse({ appointmentId: "bad", calendarId: "also-bad" }), + ); +});