From 9097a4183bb704d07b773c3ed976feaf32fa7ee6 Mon Sep 17 00:00:00 2001 From: Yadunand Prem Date: Sun, 13 Jul 2025 18:27:00 -0400 Subject: [PATCH] feat: create tables for habit and intake trackers --- api/drizzle/0001_stormy_gertrude_yorkes.sql | 86 ++ api/drizzle/meta/0001_snapshot.json | 1028 +++++++++++++++++++ api/drizzle/meta/_journal.json | 7 + api/src/db/schema/habit.ts | 53 + api/src/db/schema/intake.ts | 96 ++ 5 files changed, 1270 insertions(+) create mode 100644 api/drizzle/0001_stormy_gertrude_yorkes.sql create mode 100644 api/drizzle/meta/0001_snapshot.json create mode 100644 api/src/db/schema/habit.ts create mode 100644 api/src/db/schema/intake.ts diff --git a/api/drizzle/0001_stormy_gertrude_yorkes.sql b/api/drizzle/0001_stormy_gertrude_yorkes.sql new file mode 100644 index 0000000..2951dcd --- /dev/null +++ b/api/drizzle/0001_stormy_gertrude_yorkes.sql @@ -0,0 +1,86 @@ +CREATE SCHEMA "habit_tracker"; +--> statement-breakpoint +CREATE SCHEMA "intake_tracker"; +--> statement-breakpoint +CREATE TABLE "habit_tracker"."habit" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "name" varchar(255) NOT NULL, + "description" text, + "frequency_type" varchar(20) NOT NULL, + "target_count" integer DEFAULT 1 NOT NULL, + "interval_days" integer, + "active" boolean DEFAULT true NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "freqency_type_check" CHECK ("habit_tracker"."habit"."frequency_type" IN ('daily', 'interval', 'multi_daily')), + CONSTRAINT "target_count_check" CHECK ("habit_tracker"."habit"."target_count" > 0), + CONSTRAINT "interval_days_check" CHECK (("habit_tracker"."habit"."frequency_type" = 'interval' AND "habit_tracker"."habit"."interval_days" IS NOT NULL AND "habit_tracker"."habit"."interval_days" > 0) OR ("habit_tracker"."habit"."frequency_type" != 'interval' AND "habit_tracker"."habit"."interval_days" IS NULL)) +); +--> statement-breakpoint +CREATE TABLE "habit_tracker"."habit_completion" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "habit_id" uuid NOT NULL, + "notes" text, + "completed_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "intake_tracker"."daily_summary" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "intake_metric_id" uuid NOT NULL, + "date" date NOT NULL, + "total_value" numeric(10, 2) NOT NULL, + "entry_count" integer NOT NULL, + "first_entry_at" timestamp, + "last_entry_at" timestamp, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "daily_summary_intake_metric_id_date_unique" UNIQUE("intake_metric_id","date"), + CONSTRAINT "positive_total_check" CHECK ("intake_tracker"."daily_summary"."total_value" > 0), + CONSTRAINT "positive_count_check" CHECK ("intake_tracker"."daily_summary"."entry_count" > 0) +); +--> statement-breakpoint +CREATE TABLE "intake_tracker"."intake_metric" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "metric_type" varchar(50) NOT NULL, + "unit" varchar(20) NOT NULL, + "display_name" varchar(100) NOT NULL, + "target_value" numeric(10, 2), + "min_value" numeric(10, 2), + "max_value" numeric(10, 2), + "is_cumulative" boolean DEFAULT true NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "intake_metric_user_id_metric_type_unique" UNIQUE("user_id","metric_type"), + CONSTRAINT "positive_target_check" CHECK ("intake_tracker"."intake_metric"."target_value" IS NULL OR "intake_tracker"."intake_metric"."target_value" > 0), + CONSTRAINT "positive_min_check" CHECK ("intake_tracker"."intake_metric"."min_value" IS NULL OR "intake_tracker"."intake_metric"."min_value" >= 0), + CONSTRAINT "positive_max_check" CHECK ("intake_tracker"."intake_metric"."max_value" IS NULL OR "intake_tracker"."intake_metric"."max_value" >= 0), + CONSTRAINT "min_max_check" CHECK ("intake_tracker"."intake_metric"."min_value" IS NULL OR "intake_tracker"."intake_metric"."max_value" IS NULL OR "intake_tracker"."intake_metric"."min_value" <= "intake_tracker"."intake_metric"."max_value") +); +--> statement-breakpoint +CREATE TABLE "intake_tracker"."intake_record" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "intake_metric_id" uuid NOT NULL, + "value" numeric(10, 2), + "recorded_at" timestamp DEFAULT now() NOT NULL, + "notes" text, + "updated_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "deleted_at" timestamp, + CONSTRAINT "positive_value_check" CHECK ("intake_tracker"."intake_record"."value" > 0), + CONSTRAINT "recorded_at_not_future_check" CHECK ("intake_tracker"."intake_record"."recorded_at" <= NOW()) +); +--> statement-breakpoint +ALTER TABLE "habit_tracker"."habit" ADD CONSTRAINT "habit_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "shared"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "habit_tracker"."habit_completion" ADD CONSTRAINT "habit_completion_habit_id_habit_id_fk" FOREIGN KEY ("habit_id") REFERENCES "habit_tracker"."habit"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "intake_tracker"."daily_summary" ADD CONSTRAINT "daily_summary_intake_metric_id_intake_metric_id_fk" FOREIGN KEY ("intake_metric_id") REFERENCES "intake_tracker"."intake_metric"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "intake_tracker"."intake_metric" ADD CONSTRAINT "intake_metric_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "shared"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "intake_tracker"."intake_record" ADD CONSTRAINT "intake_record_intake_metric_id_intake_metric_id_fk" FOREIGN KEY ("intake_metric_id") REFERENCES "intake_tracker"."intake_metric"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/api/drizzle/meta/0001_snapshot.json b/api/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..c84c34b --- /dev/null +++ b/api/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1028 @@ +{ + "id": "a1af6caa-ef04-4d68-ab28-68ba8ebbaf0b", + "prevId": "6f6d04f6-92de-4c28-8736-75fafbfa3aef", + "version": "7", + "dialect": "postgresql", + "tables": { + "shared.account": { + "name": "account", + "schema": "shared", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "shared", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "shared.apikey": { + "name": "apikey", + "schema": "shared", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "tableTo": "user", + "schemaTo": "shared", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "shared.session": { + "name": "session", + "schema": "shared", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "shared", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "shared.user": { + "name": "user", + "schema": "shared", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "shared.verification": { + "name": "verification", + "schema": "shared", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "habit_tracker.habit": { + "name": "habit", + "schema": "habit_tracker", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frequency_type": { + "name": "frequency_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "target_count": { + "name": "target_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "interval_days": { + "name": "interval_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "habit_user_id_user_id_fk": { + "name": "habit_user_id_user_id_fk", + "tableFrom": "habit", + "tableTo": "user", + "schemaTo": "shared", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "freqency_type_check": { + "name": "freqency_type_check", + "value": "\"habit_tracker\".\"habit\".\"frequency_type\" IN ('daily', 'interval', 'multi_daily')" + }, + "target_count_check": { + "name": "target_count_check", + "value": "\"habit_tracker\".\"habit\".\"target_count\" > 0" + }, + "interval_days_check": { + "name": "interval_days_check", + "value": "(\"habit_tracker\".\"habit\".\"frequency_type\" = 'interval' AND \"habit_tracker\".\"habit\".\"interval_days\" IS NOT NULL AND \"habit_tracker\".\"habit\".\"interval_days\" > 0) OR (\"habit_tracker\".\"habit\".\"frequency_type\" != 'interval' AND \"habit_tracker\".\"habit\".\"interval_days\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "habit_tracker.habit_completion": { + "name": "habit_completion", + "schema": "habit_tracker", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "habit_id": { + "name": "habit_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "habit_completion_habit_id_habit_id_fk": { + "name": "habit_completion_habit_id_habit_id_fk", + "tableFrom": "habit_completion", + "tableTo": "habit", + "schemaTo": "habit_tracker", + "columnsFrom": [ + "habit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "intake_tracker.daily_summary": { + "name": "daily_summary", + "schema": "intake_tracker", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intake_metric_id": { + "name": "intake_metric_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_value": { + "name": "total_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "entry_count": { + "name": "entry_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_entry_at": { + "name": "first_entry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_entry_at": { + "name": "last_entry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "daily_summary_intake_metric_id_intake_metric_id_fk": { + "name": "daily_summary_intake_metric_id_intake_metric_id_fk", + "tableFrom": "daily_summary", + "tableTo": "intake_metric", + "schemaTo": "intake_tracker", + "columnsFrom": [ + "intake_metric_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "daily_summary_intake_metric_id_date_unique": { + "name": "daily_summary_intake_metric_id_date_unique", + "nullsNotDistinct": false, + "columns": [ + "intake_metric_id", + "date" + ] + } + }, + "policies": {}, + "checkConstraints": { + "positive_total_check": { + "name": "positive_total_check", + "value": "\"intake_tracker\".\"daily_summary\".\"total_value\" > 0" + }, + "positive_count_check": { + "name": "positive_count_check", + "value": "\"intake_tracker\".\"daily_summary\".\"entry_count\" > 0" + } + }, + "isRLSEnabled": false + }, + "intake_tracker.intake_metric": { + "name": "intake_metric", + "schema": "intake_tracker", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric_type": { + "name": "metric_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "target_value": { + "name": "target_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_cumulative": { + "name": "is_cumulative", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "intake_metric_user_id_user_id_fk": { + "name": "intake_metric_user_id_user_id_fk", + "tableFrom": "intake_metric", + "tableTo": "user", + "schemaTo": "shared", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "intake_metric_user_id_metric_type_unique": { + "name": "intake_metric_user_id_metric_type_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "metric_type" + ] + } + }, + "policies": {}, + "checkConstraints": { + "positive_target_check": { + "name": "positive_target_check", + "value": "\"intake_tracker\".\"intake_metric\".\"target_value\" IS NULL OR \"intake_tracker\".\"intake_metric\".\"target_value\" > 0" + }, + "positive_min_check": { + "name": "positive_min_check", + "value": "\"intake_tracker\".\"intake_metric\".\"min_value\" IS NULL OR \"intake_tracker\".\"intake_metric\".\"min_value\" >= 0" + }, + "positive_max_check": { + "name": "positive_max_check", + "value": "\"intake_tracker\".\"intake_metric\".\"max_value\" IS NULL OR \"intake_tracker\".\"intake_metric\".\"max_value\" >= 0" + }, + "min_max_check": { + "name": "min_max_check", + "value": "\"intake_tracker\".\"intake_metric\".\"min_value\" IS NULL OR \"intake_tracker\".\"intake_metric\".\"max_value\" IS NULL OR \"intake_tracker\".\"intake_metric\".\"min_value\" <= \"intake_tracker\".\"intake_metric\".\"max_value\"" + } + }, + "isRLSEnabled": false + }, + "intake_tracker.intake_record": { + "name": "intake_record", + "schema": "intake_tracker", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "intake_metric_id": { + "name": "intake_metric_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "intake_record_intake_metric_id_intake_metric_id_fk": { + "name": "intake_record_intake_metric_id_intake_metric_id_fk", + "tableFrom": "intake_record", + "tableTo": "intake_metric", + "schemaTo": "intake_tracker", + "columnsFrom": [ + "intake_metric_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "positive_value_check": { + "name": "positive_value_check", + "value": "\"intake_tracker\".\"intake_record\".\"value\" > 0" + }, + "recorded_at_not_future_check": { + "name": "recorded_at_not_future_check", + "value": "\"intake_tracker\".\"intake_record\".\"recorded_at\" <= NOW()" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": { + "shared": "shared", + "habit_tracker": "habit_tracker", + "intake_tracker": "intake_tracker" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json index 87a8c4f..e29132d 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1752443645228, "tag": "0000_hard_violations", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1752445599098, + "tag": "0001_stormy_gertrude_yorkes", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/db/schema/habit.ts b/api/src/db/schema/habit.ts new file mode 100644 index 0000000..00a7c85 --- /dev/null +++ b/api/src/db/schema/habit.ts @@ -0,0 +1,53 @@ +import { sql } from "drizzle-orm"; +import { + boolean, + check, + integer, + pgSchema, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { user } from "./auth"; +import { idPrimaryKey, timestampSchema } from "./helpers"; + +export const habitTracker = pgSchema("habit_tracker"); + +export const habit = habitTracker.table( + "habit", + { + id: idPrimaryKey, + userId: uuid("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + description: text("description"), + frequencyType: varchar("frequency_type", { length: 20 }).notNull(), + targetCount: integer("target_count").notNull().default(1), + intervalDays: integer("interval_days"), + active: boolean("active").notNull().default(true), + ...timestampSchema, + }, + (t) => [ + check( + "freqency_type_check", + sql`${t.frequencyType} IN ('daily', 'interval', 'multi_daily')`, + ), + check("target_count_check", sql`${t.targetCount} > 0`), + check( + "interval_days_check", + sql`(${t.frequencyType} = 'interval' AND ${t.intervalDays} IS NOT NULL AND ${t.intervalDays} > 0) OR (${t.frequencyType} != 'interval' AND ${t.intervalDays} IS NULL)`, + ), + ], +); + +export const habitCompletion = habitTracker.table("habit_completion", { + id: uuid("id").primaryKey().default(sql`gen_random_uuid()`), + habitId: uuid("habit_id") + .notNull() + .references(() => habit.id, { onDelete: "cascade" }), + notes: text("notes"), + completed_at: timestamp("completed_at").defaultNow().notNull(), + ...timestampSchema, +}); diff --git a/api/src/db/schema/intake.ts b/api/src/db/schema/intake.ts new file mode 100644 index 0000000..aa747c8 --- /dev/null +++ b/api/src/db/schema/intake.ts @@ -0,0 +1,96 @@ +import { sql } from "drizzle-orm"; +import { + boolean, + check, + date, + decimal, + integer, + pgSchema, + text, + timestamp, + unique, + uuid, + varchar, +} from "drizzle-orm/pg-core"; +import { user } from "./auth"; +import { idPrimaryKey, timestampSchema } from "./helpers"; + +export const intakeTracker = pgSchema("intake_tracker"); + +export const intakeMetric = intakeTracker.table( + "intake_metric", + { + id: idPrimaryKey, + userId: uuid("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + metricType: varchar("metric_type", { length: 50 }).notNull(), + unit: varchar("unit", { length: 20 }).notNull(), + displayName: varchar("display_name", { length: 100 }).notNull(), + + targetValue: decimal("target_value", { precision: 10, scale: 2 }), + minValue: decimal("min_value", { precision: 10, scale: 2 }), + maxValue: decimal("max_value", { precision: 10, scale: 2 }), + isCumulative: boolean("is_cumulative").notNull().default(true), + active: boolean("active").notNull().default(true), + ...timestampSchema, + }, + (t) => [ + unique().on(t.userId, t.metricType), + check( + "positive_target_check", + sql`${t.targetValue} IS NULL OR ${t.targetValue} > 0`, + ), + check( + "positive_min_check", + sql`${t.minValue} IS NULL OR ${t.minValue} >= 0`, + ), + check( + "positive_max_check", + sql`${t.maxValue} IS NULL OR ${t.maxValue} >= 0`, + ), + check( + "min_max_check", + sql`${t.minValue} IS NULL OR ${t.maxValue} IS NULL OR ${t.minValue} <= ${t.maxValue}`, + ), + ], +); + +export const intakeRecord = intakeTracker.table( + "intake_record", + { + id: idPrimaryKey, + intakeMetricId: uuid("intake_metric_id") + .notNull() + .references(() => intakeMetric.id, { onDelete: "cascade" }), + value: decimal({ precision: 10, scale: 2 }), + recordedAt: timestamp("recorded_at").notNull().defaultNow(), + notes: text("notes"), + ...timestampSchema, + }, + (t) => [ + check("positive_value_check", sql`${t.value} > 0`), + check("recorded_at_not_future_check", sql`${t.recordedAt} <= NOW()`), + ], +); + +export const dailySummary = intakeTracker.table( + "daily_summary", + { + id: idPrimaryKey, + intakeMetricId: uuid("intake_metric_id") + .notNull() + .references(() => intakeMetric.id, { onDelete: "cascade" }), + date: date("date").notNull(), + totalValue: decimal("total_value", { precision: 10, scale: 2 }).notNull(), + entryCount: integer("entry_count").notNull(), + firstEntryAt: timestamp("first_entry_at"), + lastEntryAt: timestamp("last_entry_at"), + ...timestampSchema, + }, + (t) => [ + unique().on(t.intakeMetricId, t.date), + check("positive_total_check", sql`${t.totalValue} > 0`), + check("positive_count_check", sql`${t.entryCount} > 0`), + ], +);