diff --git a/drizzle-pg.config.ts b/drizzle-pg.config.ts new file mode 100644 index 0000000..1255457 --- /dev/null +++ b/drizzle-pg.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle-pg", + schema: "./src/db/schema.ts", + dialect: "postgresql", +}); diff --git a/drizzle-pg/0000_thankful_loners.sql b/drizzle-pg/0000_thankful_loners.sql new file mode 100644 index 0000000..ef33028 --- /dev/null +++ b/drizzle-pg/0000_thankful_loners.sql @@ -0,0 +1,140 @@ +CREATE TABLE "api_keys" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "key_hash" text NOT NULL, + "key_prefix" text NOT NULL, + "user_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "categories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "icon" text DEFAULT 'package' NOT NULL, + "user_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "categories_user_id_name_unique" UNIQUE("user_id","name") +); +--> statement-breakpoint +CREATE TABLE "items" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "weight_grams" double precision, + "price_cents" integer, + "category_id" integer NOT NULL, + "user_id" integer NOT NULL, + "notes" text, + "product_url" text, + "image_filename" text, + "image_source_url" text, + "quantity" integer DEFAULT 1 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "oauth_clients" ( + "id" serial PRIMARY KEY NOT NULL, + "client_id" text NOT NULL, + "client_name" text, + "redirect_uris" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "oauth_clients_client_id_unique" UNIQUE("client_id") +); +--> statement-breakpoint +CREATE TABLE "oauth_codes" ( + "id" serial PRIMARY KEY NOT NULL, + "code" text NOT NULL, + "client_id" text NOT NULL, + "code_challenge" text NOT NULL, + "code_challenge_method" text DEFAULT 'S256' NOT NULL, + "redirect_uri" text NOT NULL, + "expires_at" timestamp NOT NULL, + "used" integer DEFAULT 0 NOT NULL, + CONSTRAINT "oauth_codes_code_unique" UNIQUE("code") +); +--> statement-breakpoint +CREATE TABLE "oauth_tokens" ( + "id" serial PRIMARY KEY NOT NULL, + "access_token_hash" text NOT NULL, + "refresh_token_hash" text NOT NULL, + "client_id" text NOT NULL, + "user_id" integer NOT NULL, + "expires_at" timestamp NOT NULL, + "refresh_expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "oauth_tokens_access_token_hash_unique" UNIQUE("access_token_hash"), + CONSTRAINT "oauth_tokens_refresh_token_hash_unique" UNIQUE("refresh_token_hash") +); +--> statement-breakpoint +CREATE TABLE "settings" ( + "user_id" integer NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + CONSTRAINT "settings_user_id_key_pk" PRIMARY KEY("user_id","key") +); +--> statement-breakpoint +CREATE TABLE "setup_items" ( + "id" serial PRIMARY KEY NOT NULL, + "setup_id" integer NOT NULL, + "item_id" integer NOT NULL, + "classification" text DEFAULT 'base' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "setups" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "user_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "thread_candidates" ( + "id" serial PRIMARY KEY NOT NULL, + "thread_id" integer NOT NULL, + "name" text NOT NULL, + "weight_grams" double precision, + "price_cents" integer, + "category_id" integer NOT NULL, + "notes" text, + "product_url" text, + "image_filename" text, + "image_source_url" text, + "status" text DEFAULT 'researching' NOT NULL, + "pros" text, + "cons" text, + "sort_order" double precision DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "threads" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "status" text DEFAULT 'active' NOT NULL, + "resolved_candidate_id" integer, + "category_id" integer NOT NULL, + "user_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" serial PRIMARY KEY NOT NULL, + "logto_sub" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_logto_sub_unique" UNIQUE("logto_sub") +); +--> statement-breakpoint +ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "categories" ADD CONSTRAINT "categories_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "items" ADD CONSTRAINT "items_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "items" ADD CONSTRAINT "items_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "oauth_tokens" ADD CONSTRAINT "oauth_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "settings" ADD CONSTRAINT "settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "setup_items" ADD CONSTRAINT "setup_items_setup_id_setups_id_fk" FOREIGN KEY ("setup_id") REFERENCES "public"."setups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "setup_items" ADD CONSTRAINT "setup_items_item_id_items_id_fk" FOREIGN KEY ("item_id") REFERENCES "public"."items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "setups" ADD CONSTRAINT "setups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "thread_candidates" ADD CONSTRAINT "thread_candidates_thread_id_threads_id_fk" FOREIGN KEY ("thread_id") REFERENCES "public"."threads"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "thread_candidates" ADD CONSTRAINT "thread_candidates_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "threads" ADD CONSTRAINT "threads_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "threads" ADD CONSTRAINT "threads_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle-pg/meta/0000_snapshot.json b/drizzle-pg/meta/0000_snapshot.json new file mode 100644 index 0000000..fd0b859 --- /dev/null +++ b/drizzle-pg/meta/0000_snapshot.json @@ -0,0 +1,934 @@ +{ + "id": "2f3f44c0-0fd3-4ac5-b1fb-51bc709342df", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'package'" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "categories_user_id_users_id_fk": { + "name": "categories_user_id_users_id_fk", + "tableFrom": "categories", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "categories_user_id_name_unique": { + "name": "categories_user_id_name_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.items": { + "name": "items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight_grams": { + "name": "weight_grams", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_source_url": { + "name": "image_source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "items_category_id_categories_id_fk": { + "name": "items_category_id_categories_id_fk", + "tableFrom": "items", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "items_user_id_users_id_fk": { + "name": "items_user_id_users_id_fk", + "tableFrom": "items", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_clients_client_id_unique": { + "name": "oauth_clients_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_codes": { + "name": "oauth_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'S256'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_codes_code_unique": { + "name": "oauth_codes_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_tokens": { + "name": "oauth_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_hash": { + "name": "refresh_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_expires_at": { + "name": "refresh_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_tokens_user_id_users_id_fk": { + "name": "oauth_tokens_user_id_users_id_fk", + "tableFrom": "oauth_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_tokens_access_token_hash_unique": { + "name": "oauth_tokens_access_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "access_token_hash" + ] + }, + "oauth_tokens_refresh_token_hash_unique": { + "name": "oauth_tokens_refresh_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_users_id_fk": { + "name": "settings_user_id_users_id_fk", + "tableFrom": "settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "settings_user_id_key_pk": { + "name": "settings_user_id_key_pk", + "columns": [ + "user_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setup_items": { + "name": "setup_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'base'" + } + }, + "indexes": {}, + "foreignKeys": { + "setup_items_setup_id_setups_id_fk": { + "name": "setup_items_setup_id_setups_id_fk", + "tableFrom": "setup_items", + "tableTo": "setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "setup_items_item_id_items_id_fk": { + "name": "setup_items_item_id_items_id_fk", + "tableFrom": "setup_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.setups": { + "name": "setups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "setups_user_id_users_id_fk": { + "name": "setups_user_id_users_id_fk", + "tableFrom": "setups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thread_candidates": { + "name": "thread_candidates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight_grams": { + "name": "weight_grams", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image_source_url": { + "name": "image_source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'researching'" + }, + "pros": { + "name": "pros", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cons": { + "name": "cons", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "thread_candidates_thread_id_threads_id_fk": { + "name": "thread_candidates_thread_id_threads_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thread_candidates_category_id_categories_id_fk": { + "name": "thread_candidates_category_id_categories_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.threads": { + "name": "threads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "resolved_candidate_id": { + "name": "resolved_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "threads_category_id_categories_id_fk": { + "name": "threads_category_id_categories_id_fk", + "tableFrom": "threads", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "threads_user_id_users_id_fk": { + "name": "threads_user_id_users_id_fk", + "tableFrom": "threads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "logto_sub": { + "name": "logto_sub", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_logto_sub_unique": { + "name": "users_logto_sub_unique", + "nullsNotDistinct": false, + "columns": [ + "logto_sub" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json new file mode 100644 index 0000000..7da9303 --- /dev/null +++ b/drizzle-pg/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775377947759, + "tag": "0000_thankful_loners", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 9e7113b..8206518 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,58 +1,86 @@ -import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { + doublePrecision, + integer, + pgTable, + primaryKey, + serial, + text, + timestamp, + unique, +} from "drizzle-orm/pg-core"; -export const categories = sqliteTable("categories", { - id: integer("id").primaryKey({ autoIncrement: true }), - name: text("name").notNull().unique(), - icon: text("icon").notNull().default("package"), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), +// ── Users ─────────────────────────────────────────────────────────── + +export const users = pgTable("users", { + id: serial("id").primaryKey(), + logtoSub: text("logto_sub").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), }); -export const items = sqliteTable("items", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── Categories ────────────────────────────────────────────────────── + +export const categories = pgTable( + "categories", + { + id: serial("id").primaryKey(), + name: text("name").notNull(), + icon: text("icon").notNull().default("package"), + userId: integer("user_id") + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => [unique().on(table.userId, table.name)], +); + +// ── Items ─────────────────────────────────────────────────────────── + +export const items = pgTable("items", { + id: serial("id").primaryKey(), name: text("name").notNull(), - weightGrams: real("weight_grams"), + weightGrams: doublePrecision("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() .references(() => categories.id), + userId: integer("user_id") + .notNull() + .references(() => users.id), notes: text("notes"), productUrl: text("product_url"), imageFilename: text("image_filename"), imageSourceUrl: text("image_source_url"), quantity: integer("quantity").notNull().default(1), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), }); -export const threads = sqliteTable("threads", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── Threads ───────────────────────────────────────────────────────── + +export const threads = pgTable("threads", { + id: serial("id").primaryKey(), name: text("name").notNull(), status: text("status").notNull().default("active"), resolvedCandidateId: integer("resolved_candidate_id"), categoryId: integer("category_id") .notNull() .references(() => categories.id), - createdAt: integer("created_at", { mode: "timestamp" }) + userId: integer("user_id") .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + .references(() => users.id), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), }); -export const threadCandidates = sqliteTable("thread_candidates", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── Thread Candidates ─────────────────────────────────────────────── + +export const threadCandidates = pgTable("thread_candidates", { + id: serial("id").primaryKey(), threadId: integer("thread_id") .notNull() .references(() => threads.id, { onDelete: "cascade" }), name: text("name").notNull(), - weightGrams: real("weight_grams"), + weightGrams: doublePrecision("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() @@ -64,28 +92,27 @@ export const threadCandidates = sqliteTable("thread_candidates", { status: text("status").notNull().default("researching"), pros: text("pros"), cons: text("cons"), - sortOrder: real("sort_order").notNull().default(0), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + sortOrder: doublePrecision("sort_order").notNull().default(0), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), }); -export const setups = sqliteTable("setups", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── Setups ────────────────────────────────────────────────────────── + +export const setups = pgTable("setups", { + id: serial("id").primaryKey(), name: text("name").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) + userId: integer("user_id") .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + .references(() => users.id), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), }); -export const setupItems = sqliteTable("setup_items", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── Setup Items ───────────────────────────────────────────────────── + +export const setupItems = pgTable("setup_items", { + id: serial("id").primaryKey(), setupId: integer("setup_id") .notNull() .references(() => setups.id, { onDelete: "cascade" }), @@ -95,69 +122,69 @@ export const setupItems = sqliteTable("setup_items", { classification: text("classification").notNull().default("base"), }); -export const settings = sqliteTable("settings", { - key: text("key").primaryKey(), - value: text("value").notNull(), -}); +// ── Settings ──────────────────────────────────────────────────────── -export const users = sqliteTable("users", { - id: integer("id").primaryKey({ autoIncrement: true }), - username: text("username").notNull().unique(), - passwordHash: text("password_hash").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), -}); +export const settings = pgTable( + "settings", + { + userId: integer("user_id") + .notNull() + .references(() => users.id), + key: text("key").notNull(), + value: text("value").notNull(), + }, + (table) => [primaryKey({ columns: [table.userId, table.key] })], +); -export const sessions = sqliteTable("sessions", { - id: text("id").primaryKey(), - userId: integer("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), -}); +// ── API Keys ──────────────────────────────────────────────────────── -export const apiKeys = sqliteTable("api_keys", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const apiKeys = pgTable("api_keys", { + id: serial("id").primaryKey(), name: text("name").notNull(), keyHash: text("key_hash").notNull(), keyPrefix: text("key_prefix").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) + userId: integer("user_id") .notNull() - .$defaultFn(() => new Date()), + .references(() => users.id), + createdAt: timestamp("created_at").defaultNow().notNull(), }); -export const oauthClients = sqliteTable("oauth_clients", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── OAuth Clients ─────────────────────────────────────────────────── + +export const oauthClients = pgTable("oauth_clients", { + id: serial("id").primaryKey(), clientId: text("client_id").notNull().unique(), clientName: text("client_name"), redirectUris: text("redirect_uris").notNull(), // JSON array - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").defaultNow().notNull(), }); -export const oauthCodes = sqliteTable("oauth_codes", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── OAuth Authorization Codes ─────────────────────────────────────── + +export const oauthCodes = pgTable("oauth_codes", { + id: serial("id").primaryKey(), code: text("code").notNull().unique(), clientId: text("client_id").notNull(), codeChallenge: text("code_challenge").notNull(), - codeChallengeMethod: text("code_challenge_method").notNull().default("S256"), + codeChallengeMethod: text("code_challenge_method") + .notNull() + .default("S256"), redirectUri: text("redirect_uri").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + expiresAt: timestamp("expires_at").notNull(), used: integer("used").notNull().default(0), }); -export const oauthTokens = sqliteTable("oauth_tokens", { - id: integer("id").primaryKey({ autoIncrement: true }), +// ── OAuth Tokens ──────────────────────────────────────────────────── + +export const oauthTokens = pgTable("oauth_tokens", { + id: serial("id").primaryKey(), accessTokenHash: text("access_token_hash").notNull().unique(), refreshTokenHash: text("refresh_token_hash").notNull().unique(), clientId: text("client_id").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // access token expiry - refreshExpiresAt: integer("refresh_expires_at", { - mode: "timestamp", - }).notNull(), // refresh token expiry - createdAt: integer("created_at", { mode: "timestamp" }) + userId: integer("user_id") .notNull() - .$defaultFn(() => new Date()), + .references(() => users.id), + expiresAt: timestamp("expires_at").notNull(), + refreshExpiresAt: timestamp("refresh_expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), }); diff --git a/src/db/seed.ts b/src/db/seed.ts index c7cf900..960ade6 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,14 +1,4 @@ -import { db } from "./index.ts"; -import { categories } from "./schema.ts"; - export function seedDefaults() { - const existing = db.select().from(categories).all(); - if (existing.length === 0) { - db.insert(categories) - .values({ - name: "Uncategorized", - icon: "package", - }) - .run(); - } + // Per-user default categories are created on first login (Phase 16) + // The getOrCreateUncategorized helper in category.service.ts handles this lazily. }