Merge pull request #10 from semihyesilyurt/dev

feat(FN-190): move drizzle-kit to production dependencies (+8 more)
This commit is contained in:
Semih Yeşilyurt
2026-05-12 07:35:06 +03:00
committed by GitHub
11 changed files with 6220 additions and 7 deletions

View File

@@ -53,7 +53,7 @@ jobs:
cd /home/${{ secrets.SSH_USER }}/ss
echo "$(date '+%Y-%m-%d %H:%M:%S') - Running database migrations..."
cd apps/api && pnpm db:push && cd ../..
cd apps/api && pnpm db:migrate && cd ../..
echo "$(date '+%Y-%m-%d %H:%M:%S') - Reloading PM2 processes..."
pm2 reload ecosystem.config.js

View File

@@ -59,6 +59,10 @@ COPY --from=build /app/packages/ui/package.json ./packages/ui/
# NestJS CLI config (needed for serve-static)
COPY --from=build /app/apps/api/nest-cli.json ./apps/api/
# Drizzle migrations + startup wrapper
COPY --from=build /app/apps/api/drizzle ./apps/api/drizzle
COPY --from=build /app/apps/api/start.sh ./apps/api/start.sh
RUN corepack enable && corepack prepare pnpm@10.29.3 --activate \
&& pnpm install --frozen-lockfile --prod \
&& pnpm store prune
@@ -66,4 +70,4 @@ RUN corepack enable && corepack prepare pnpm@10.29.3 --activate \
ENV NODE_ENV=production
EXPOSE 4000
CMD ["node", "--enable-source-maps", "apps/api/dist/main.js"]
CMD ["./apps/api/start.sh"]

View File

@@ -0,0 +1,658 @@
CREATE TABLE "accounts" (
"id" text PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"account_id" text NOT NULL,
"provider_id" varchar(50) NOT NULL,
"access_token" text,
"refresh_token" text,
"access_token_expires_at" timestamp with time zone,
"refresh_token_expires_at" timestamp with time zone,
"scope" text,
"id_token" text,
"password" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "brands" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(100) NOT NULL,
"slug" varchar(100) NOT NULL,
"logo_url" text,
"is_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
);
--> statement-breakpoint
CREATE TABLE "catalog_vehicles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"source" varchar(20) DEFAULT 'pl24' NOT NULL,
"service_name" varchar(100) NOT NULL,
"brand_name" varchar(100) NOT NULL,
"brand_id" uuid,
"model" varchar(255) NOT NULL,
"year" varchar(50),
"engine" varchar(255),
"body_type" varchar(100),
"transmission" varchar(100),
"market" varchar(100),
"service_vehicle_id" varchar(255) NOT NULL,
"catalog_path" text,
"architecture" varchar(30),
"metadata" jsonb,
"categories_fetched" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "categories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"vehicle_id" uuid,
"catalog_vehicle_id" uuid,
"name" varchar(500) NOT NULL,
"name_original" varchar(500),
"parent_id" uuid,
"external_id" text,
"link_path" text,
"link_wid" varchar(100),
"unavailable" boolean DEFAULT false NOT NULL,
"source" varchar(20) DEFAULT 'pl24' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "changelog_entries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"stage" varchar(10) DEFAULT 'prod' NOT NULL,
"title" varchar(255) NOT NULL,
"description" text NOT NULL,
"published_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_category_translations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"original_name" varchar(500) NOT NULL,
"translated_name" varchar(500) NOT NULL,
"is_manual" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "oem_code_copies" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"oem_code" varchar(100) NOT NULL,
"part_id" uuid,
"vehicle_id" uuid,
"category_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "parts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"vehicle_id" uuid,
"catalog_vehicle_id" uuid,
"category_id" uuid NOT NULL,
"oem_code" varchar(100) NOT NULL,
"name" varchar(500) NOT NULL,
"name_original" varchar(500),
"description" text,
"quantity" integer,
"position" varchar(100),
"hotspot_index" integer,
"unavailable" boolean DEFAULT false NOT NULL,
"remark" text,
"model_codes" varchar(500),
"presel" boolean DEFAULT false NOT NULL,
"price" numeric(10, 2),
"currency" varchar(3),
"source" varchar(20) DEFAULT 'pl24' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "password_reset_tokens" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"token" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"used_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "payments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"subscription_id" uuid NOT NULL,
"amount" integer NOT NULL,
"currency" varchar(3) DEFAULT 'TRY' NOT NULL,
"method" varchar(20) NOT NULL,
"status" varchar(20) DEFAULT 'pending' NOT NULL,
"iyzico_payment_id" text,
"eft_receipt_url" text,
"admin_note" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "plans" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(100) NOT NULL,
"brand_count" integer NOT NULL,
"price_monthly" integer NOT NULL,
"price_yearly" integer NOT NULL,
"is_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
);
--> statement-breakpoint
CREATE TABLE "query_logs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"vin" varchar(17) NOT NULL,
"brand_id" uuid,
"source" varchar(20),
"success" boolean DEFAULT true NOT NULL,
"error_message" text,
"response_time_ms" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "referrals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"referrer_id" uuid NOT NULL,
"referred_id" uuid NOT NULL,
"reward_applied" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "schema_pics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"category_id" uuid NOT NULL,
"image_url" text NOT NULL,
"hotspots" jsonb DEFAULT '[]' NOT NULL,
"source" varchar(20) DEFAULT 'pl24' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sessions" (
"id" text PRIMARY KEY NOT NULL,
"user_id" uuid NOT NULL,
"token" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"ip_address" varchar(45),
"user_agent" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_brands" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"subscription_id" uuid NOT NULL,
"brand_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_subscriptions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"plan_id" uuid NOT NULL,
"status" varchar(20) DEFAULT 'pending' NOT NULL,
"billing_period" varchar(10) DEFAULT 'monthly' NOT NULL,
"start_date" timestamp with time zone,
"end_date" timestamp with time zone,
"cancelled_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_vehicles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"vehicle_id" uuid NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_accessed_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"email" varchar(255) NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"role" varchar(20) DEFAULT 'user' NOT NULL,
"referral_code" varchar(20),
"referred_by" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "vehicles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"vin" varchar(17) NOT NULL,
"brand_id" uuid,
"brand_name" varchar(100),
"model" varchar(255),
"year" integer,
"engine" varchar(255),
"transmission" varchar(100),
"body_type" varchar(100),
"market" varchar(100),
"raw_data" jsonb,
"source" varchar(20) DEFAULT 'pl24' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_catalogs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"catalog_id" varchar(100) NOT NULL,
"code" varchar(50),
"brand_name" varchar(100) NOT NULL,
"description" text,
"source_id" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_part_groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_catalog_id" uuid,
"group_id" varchar(100) NOT NULL,
"name" varchar(500) NOT NULL,
"name_original" varchar(500),
"parent_group_id" varchar(100),
"sort_order" integer,
"has_parts" boolean DEFAULT false,
"has_children" boolean DEFAULT false,
"source_id" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_part_images" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_part_id" uuid,
"image_url" text NOT NULL,
"original_url" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_part_numbers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_part_id" uuid,
"oem_code" varchar(100) NOT NULL,
"is_main" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_parts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_catalog_id" uuid,
"group_id" uuid,
"part_id" varchar(100),
"part_number" varchar(100),
"name" varchar(500) NOT NULL,
"name_original" varchar(500),
"description" text,
"oem_number" varchar(100),
"hotspot_index" integer,
"raw_data" jsonb,
"source_id" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_schema_pics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"group_id" uuid,
"image_url" text,
"original_url" text,
"local_path" varchar(500),
"hotspots" jsonb DEFAULT '[]' NOT NULL,
"width" integer,
"height" integer,
"sort_order" integer DEFAULT 0,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_scrape_sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"vin" varchar(17) NOT NULL,
"status" varchar(20) DEFAULT 'pending' NOT NULL,
"job_id" varchar(100),
"result" jsonb,
"error_message" text,
"started_at" timestamp with time zone,
"completed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_vehicle_group_links" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_vehicle_id" uuid NOT NULL,
"emex_group_id" uuid NOT NULL,
"ssd" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_vehicle_part_links" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_vehicle_id" uuid NOT NULL,
"emex_part_id" uuid NOT NULL,
"emex_group_id" uuid,
"quantity" integer,
"position" varchar(100),
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_vehicle_vins" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"emex_vehicle_id" uuid,
"vin" varchar(17) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "emex_vehicles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"catalog_id" uuid,
"vehicle_id" varchar(100) NOT NULL,
"name" varchar(500),
"model_code" varchar(100),
"engine" varchar(255),
"engine_code" varchar(100),
"body_type" varchar(100),
"transmission" varchar(100),
"drive_type" varchar(100),
"fuel_type" varchar(100),
"year_from" integer,
"year_to" integer,
"ssd" text,
"options_raw" text,
"raw_data" jsonb,
"source_id" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pcat_part_groups" (
"id" text PRIMARY KEY NOT NULL,
"catalog_id" text NOT NULL,
"parent_id" text,
"name" text NOT NULL,
"img_url" text,
"has_subgroups" boolean DEFAULT false NOT NULL,
"has_parts" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pcat_parts" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"notice" text,
"description" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pcat_schema_pics" (
"id" serial PRIMARY KEY NOT NULL,
"group_id" text NOT NULL,
"car_id" text NOT NULL,
"img_url" text NOT NULL,
"img_description" text,
"hotspots" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pcat_vehicles" (
"id" text PRIMARY KEY NOT NULL,
"catalog_id" text NOT NULL,
"vin" varchar(17),
"name" text NOT NULL,
"description" text,
"parameters" jsonb,
"raw_data" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_catalogs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"catalog_id" varchar(100) NOT NULL,
"brand_name" varchar(100) NOT NULL,
"description" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_part_groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_vehicle_id" uuid,
"group_id" varchar(100) NOT NULL,
"name" varchar(500) NOT NULL,
"parent_group_id" varchar(100),
"sort_order" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_part_images" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_part_id" uuid,
"image_url" text NOT NULL,
"original_url" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_part_numbers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_part_id" uuid,
"oem_code" varchar(100) NOT NULL,
"is_main" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_parts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_vehicle_id" uuid,
"group_id" uuid,
"part_id" varchar(100),
"name" varchar(500) NOT NULL,
"description" text,
"quantity" integer,
"position" varchar(100),
"hotspot_index" integer,
"raw_data" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_schema_pics" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"group_id" uuid,
"image_url" text NOT NULL,
"original_url" text,
"hotspots" jsonb DEFAULT '[]' NOT NULL,
"width" integer,
"height" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_vehicle_groups" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"catalog_id" uuid,
"group_id" varchar(100) NOT NULL,
"name" varchar(500) NOT NULL,
"parent_group_id" varchar(100),
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_vehicle_parts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_vehicle_id" uuid,
"pl24_part_id" uuid,
"fitment_info" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_vehicle_vins" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"pl24_vehicle_id" uuid,
"vin" varchar(17) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "pl24_vehicles" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"catalog_id" uuid,
"vehicle_id" varchar(100) NOT NULL,
"name" varchar(500),
"model_code" varchar(100),
"engine" varchar(255),
"transmission" varchar(100),
"body_type" varchar(100),
"market" varchar(100),
"year_from" integer,
"year_to" integer,
"raw_data" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "catalog_vehicles" ADD CONSTRAINT "catalog_vehicles_brand_id_brands_id_fk" FOREIGN KEY ("brand_id") REFERENCES "public"."brands"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "categories" ADD CONSTRAINT "categories_vehicle_id_vehicles_id_fk" FOREIGN KEY ("vehicle_id") REFERENCES "public"."vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "categories" ADD CONSTRAINT "categories_catalog_vehicle_id_catalog_vehicles_id_fk" FOREIGN KEY ("catalog_vehicle_id") REFERENCES "public"."catalog_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "oem_code_copies" ADD CONSTRAINT "oem_code_copies_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "parts" ADD CONSTRAINT "parts_vehicle_id_vehicles_id_fk" FOREIGN KEY ("vehicle_id") REFERENCES "public"."vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "parts" ADD CONSTRAINT "parts_catalog_vehicle_id_catalog_vehicles_id_fk" FOREIGN KEY ("catalog_vehicle_id") REFERENCES "public"."catalog_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "parts" ADD CONSTRAINT "parts_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "password_reset_tokens" ADD CONSTRAINT "password_reset_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "payments" ADD CONSTRAINT "payments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "payments" ADD CONSTRAINT "payments_subscription_id_user_subscriptions_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."user_subscriptions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "query_logs" ADD CONSTRAINT "query_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "query_logs" ADD CONSTRAINT "query_logs_brand_id_brands_id_fk" FOREIGN KEY ("brand_id") REFERENCES "public"."brands"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "referrals" ADD CONSTRAINT "referrals_referrer_id_users_id_fk" FOREIGN KEY ("referrer_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "referrals" ADD CONSTRAINT "referrals_referred_id_users_id_fk" FOREIGN KEY ("referred_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "schema_pics" ADD CONSTRAINT "schema_pics_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_brands" ADD CONSTRAINT "user_brands_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_brands" ADD CONSTRAINT "user_brands_subscription_id_user_subscriptions_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."user_subscriptions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_brands" ADD CONSTRAINT "user_brands_brand_id_brands_id_fk" FOREIGN KEY ("brand_id") REFERENCES "public"."brands"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_subscriptions" ADD CONSTRAINT "user_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_subscriptions" ADD CONSTRAINT "user_subscriptions_plan_id_plans_id_fk" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_vehicles" ADD CONSTRAINT "user_vehicles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_vehicles" ADD CONSTRAINT "user_vehicles_vehicle_id_vehicles_id_fk" FOREIGN KEY ("vehicle_id") REFERENCES "public"."vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_brand_id_brands_id_fk" FOREIGN KEY ("brand_id") REFERENCES "public"."brands"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_part_groups" ADD CONSTRAINT "emex_part_groups_emex_catalog_id_emex_catalogs_id_fk" FOREIGN KEY ("emex_catalog_id") REFERENCES "public"."emex_catalogs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_part_images" ADD CONSTRAINT "emex_part_images_emex_part_id_emex_parts_id_fk" FOREIGN KEY ("emex_part_id") REFERENCES "public"."emex_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_part_numbers" ADD CONSTRAINT "emex_part_numbers_emex_part_id_emex_parts_id_fk" FOREIGN KEY ("emex_part_id") REFERENCES "public"."emex_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_parts" ADD CONSTRAINT "emex_parts_emex_catalog_id_emex_catalogs_id_fk" FOREIGN KEY ("emex_catalog_id") REFERENCES "public"."emex_catalogs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_parts" ADD CONSTRAINT "emex_parts_group_id_emex_part_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."emex_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_schema_pics" ADD CONSTRAINT "emex_schema_pics_group_id_emex_part_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."emex_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_group_links" ADD CONSTRAINT "emex_vehicle_group_links_emex_vehicle_id_emex_vehicles_id_fk" FOREIGN KEY ("emex_vehicle_id") REFERENCES "public"."emex_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_group_links" ADD CONSTRAINT "emex_vehicle_group_links_emex_group_id_emex_part_groups_id_fk" FOREIGN KEY ("emex_group_id") REFERENCES "public"."emex_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_part_links" ADD CONSTRAINT "emex_vehicle_part_links_emex_vehicle_id_emex_vehicles_id_fk" FOREIGN KEY ("emex_vehicle_id") REFERENCES "public"."emex_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_part_links" ADD CONSTRAINT "emex_vehicle_part_links_emex_part_id_emex_parts_id_fk" FOREIGN KEY ("emex_part_id") REFERENCES "public"."emex_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_part_links" ADD CONSTRAINT "emex_vehicle_part_links_emex_group_id_emex_part_groups_id_fk" FOREIGN KEY ("emex_group_id") REFERENCES "public"."emex_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicle_vins" ADD CONSTRAINT "emex_vehicle_vins_emex_vehicle_id_emex_vehicles_id_fk" FOREIGN KEY ("emex_vehicle_id") REFERENCES "public"."emex_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "emex_vehicles" ADD CONSTRAINT "emex_vehicles_catalog_id_emex_catalogs_id_fk" FOREIGN KEY ("catalog_id") REFERENCES "public"."emex_catalogs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_part_groups" ADD CONSTRAINT "pl24_part_groups_pl24_vehicle_id_pl24_vehicles_id_fk" FOREIGN KEY ("pl24_vehicle_id") REFERENCES "public"."pl24_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_part_images" ADD CONSTRAINT "pl24_part_images_pl24_part_id_pl24_parts_id_fk" FOREIGN KEY ("pl24_part_id") REFERENCES "public"."pl24_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_part_numbers" ADD CONSTRAINT "pl24_part_numbers_pl24_part_id_pl24_parts_id_fk" FOREIGN KEY ("pl24_part_id") REFERENCES "public"."pl24_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_parts" ADD CONSTRAINT "pl24_parts_pl24_vehicle_id_pl24_vehicles_id_fk" FOREIGN KEY ("pl24_vehicle_id") REFERENCES "public"."pl24_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_parts" ADD CONSTRAINT "pl24_parts_group_id_pl24_part_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."pl24_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_schema_pics" ADD CONSTRAINT "pl24_schema_pics_group_id_pl24_part_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."pl24_part_groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_vehicle_groups" ADD CONSTRAINT "pl24_vehicle_groups_catalog_id_pl24_catalogs_id_fk" FOREIGN KEY ("catalog_id") REFERENCES "public"."pl24_catalogs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_vehicle_parts" ADD CONSTRAINT "pl24_vehicle_parts_pl24_vehicle_id_pl24_vehicles_id_fk" FOREIGN KEY ("pl24_vehicle_id") REFERENCES "public"."pl24_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_vehicle_parts" ADD CONSTRAINT "pl24_vehicle_parts_pl24_part_id_pl24_parts_id_fk" FOREIGN KEY ("pl24_part_id") REFERENCES "public"."pl24_parts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_vehicle_vins" ADD CONSTRAINT "pl24_vehicle_vins_pl24_vehicle_id_pl24_vehicles_id_fk" FOREIGN KEY ("pl24_vehicle_id") REFERENCES "public"."pl24_vehicles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "pl24_vehicles" ADD CONSTRAINT "pl24_vehicles_catalog_id_pl24_catalogs_id_fk" FOREIGN KEY ("catalog_id") REFERENCES "public"."pl24_catalogs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "accounts_user_id_idx" ON "accounts" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "brands_slug_idx" ON "brands" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "catalog_vehicles_source_svc_vid_idx" ON "catalog_vehicles" USING btree ("source","service_name","service_vehicle_id");--> statement-breakpoint
CREATE INDEX "catalog_vehicles_brand_name_idx" ON "catalog_vehicles" USING btree ("brand_name");--> statement-breakpoint
CREATE INDEX "catalog_vehicles_service_name_idx" ON "catalog_vehicles" USING btree ("service_name");--> statement-breakpoint
CREATE INDEX "categories_vehicle_id_idx" ON "categories" USING btree ("vehicle_id");--> statement-breakpoint
CREATE INDEX "categories_catalog_vehicle_id_idx" ON "categories" USING btree ("catalog_vehicle_id");--> statement-breakpoint
CREATE INDEX "categories_parent_id_idx" ON "categories" USING btree ("parent_id");--> statement-breakpoint
CREATE UNIQUE INDEX "categories_vehicle_name_source_idx" ON "categories" USING btree ("vehicle_id","catalog_vehicle_id","name","source");--> statement-breakpoint
CREATE INDEX "changelog_entries_published_at_idx" ON "changelog_entries" USING btree ("published_at");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_translations_original_name_idx" ON "emex_category_translations" USING btree ("original_name");--> statement-breakpoint
CREATE INDEX "oem_code_copies_user_id_created_at_idx" ON "oem_code_copies" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE INDEX "oem_code_copies_oem_code_idx" ON "oem_code_copies" USING btree ("oem_code");--> statement-breakpoint
CREATE INDEX "oem_code_copies_created_at_idx" ON "oem_code_copies" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "parts_vehicle_id_idx" ON "parts" USING btree ("vehicle_id");--> statement-breakpoint
CREATE INDEX "parts_catalog_vehicle_id_idx" ON "parts" USING btree ("catalog_vehicle_id");--> statement-breakpoint
CREATE INDEX "parts_category_id_idx" ON "parts" USING btree ("category_id");--> statement-breakpoint
CREATE INDEX "parts_oem_code_idx" ON "parts" USING btree ("oem_code");--> statement-breakpoint
CREATE UNIQUE INDEX "password_reset_tokens_token_idx" ON "password_reset_tokens" USING btree ("token");--> statement-breakpoint
CREATE INDEX "payments_user_id_idx" ON "payments" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "payments_status_idx" ON "payments" USING btree ("status");--> statement-breakpoint
CREATE INDEX "query_logs_user_id_created_at_idx" ON "query_logs" USING btree ("user_id","created_at");--> statement-breakpoint
CREATE INDEX "query_logs_vin_idx" ON "query_logs" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "referrals_referrer_id_idx" ON "referrals" USING btree ("referrer_id");--> statement-breakpoint
CREATE UNIQUE INDEX "referrals_referred_id_idx" ON "referrals" USING btree ("referred_id");--> statement-breakpoint
CREATE INDEX "schema_pics_category_id_idx" ON "schema_pics" USING btree ("category_id");--> statement-breakpoint
CREATE UNIQUE INDEX "sessions_token_idx" ON "sessions" USING btree ("token");--> statement-breakpoint
CREATE INDEX "user_brands_user_id_idx" ON "user_brands" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "user_brands_unique_idx" ON "user_brands" USING btree ("user_id","subscription_id","brand_id");--> statement-breakpoint
CREATE INDEX "user_subscriptions_user_id_idx" ON "user_subscriptions" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "user_subscriptions_status_idx" ON "user_subscriptions" USING btree ("status");--> statement-breakpoint
CREATE UNIQUE INDEX "user_vehicles_user_vehicle_idx" ON "user_vehicles" USING btree ("user_id","vehicle_id");--> statement-breakpoint
CREATE INDEX "user_vehicles_user_id_idx" ON "user_vehicles" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "users_referral_code_idx" ON "users" USING btree ("referral_code");--> statement-breakpoint
CREATE UNIQUE INDEX "vehicles_vin_unique_idx" ON "vehicles" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "verifications_identifier_idx" ON "verifications" USING btree ("identifier");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_catalogs_catalog_id_idx" ON "emex_catalogs" USING btree ("catalog_id");--> statement-breakpoint
CREATE INDEX "emex_catalogs_code_idx" ON "emex_catalogs" USING btree ("code");--> statement-breakpoint
CREATE INDEX "emex_part_groups_catalog_id_idx" ON "emex_part_groups" USING btree ("emex_catalog_id");--> statement-breakpoint
CREATE INDEX "emex_part_groups_group_id_idx" ON "emex_part_groups" USING btree ("group_id");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_part_groups_catalog_group_idx" ON "emex_part_groups" USING btree ("emex_catalog_id","group_id");--> statement-breakpoint
CREATE INDEX "emex_part_images_part_id_idx" ON "emex_part_images" USING btree ("emex_part_id");--> statement-breakpoint
CREATE INDEX "emex_part_numbers_part_id_idx" ON "emex_part_numbers" USING btree ("emex_part_id");--> statement-breakpoint
CREATE INDEX "emex_part_numbers_oem_code_idx" ON "emex_part_numbers" USING btree ("oem_code");--> statement-breakpoint
CREATE INDEX "emex_parts_catalog_id_idx" ON "emex_parts" USING btree ("emex_catalog_id");--> statement-breakpoint
CREATE INDEX "emex_parts_group_id_idx" ON "emex_parts" USING btree ("group_id");--> statement-breakpoint
CREATE INDEX "emex_parts_part_number_idx" ON "emex_parts" USING btree ("part_number");--> statement-breakpoint
CREATE INDEX "emex_parts_oem_number_idx" ON "emex_parts" USING btree ("oem_number");--> statement-breakpoint
CREATE INDEX "emex_schema_pics_group_id_idx" ON "emex_schema_pics" USING btree ("group_id");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_schema_pics_group_path_idx" ON "emex_schema_pics" USING btree ("group_id","local_path");--> statement-breakpoint
CREATE INDEX "emex_scrape_sessions_vin_idx" ON "emex_scrape_sessions" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "emex_scrape_sessions_status_idx" ON "emex_scrape_sessions" USING btree ("status");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_vgl_vehicle_group_idx" ON "emex_vehicle_group_links" USING btree ("emex_vehicle_id","emex_group_id");--> statement-breakpoint
CREATE INDEX "emex_vgl_group_idx" ON "emex_vehicle_group_links" USING btree ("emex_group_id");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_vpl_vehicle_part_group_idx" ON "emex_vehicle_part_links" USING btree ("emex_vehicle_id","emex_part_id","emex_group_id");--> statement-breakpoint
CREATE INDEX "emex_vpl_part_idx" ON "emex_vehicle_part_links" USING btree ("emex_part_id");--> statement-breakpoint
CREATE INDEX "emex_vpl_group_idx" ON "emex_vehicle_part_links" USING btree ("emex_group_id");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_vehicle_vins_vin_idx" ON "emex_vehicle_vins" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "emex_vehicle_vins_vehicle_id_idx" ON "emex_vehicle_vins" USING btree ("emex_vehicle_id");--> statement-breakpoint
CREATE UNIQUE INDEX "emex_vehicles_vehicle_id_idx" ON "emex_vehicles" USING btree ("vehicle_id");--> statement-breakpoint
CREATE INDEX "emex_vehicles_catalog_id_idx" ON "emex_vehicles" USING btree ("catalog_id");--> statement-breakpoint
CREATE INDEX "emex_vehicles_name_idx" ON "emex_vehicles" USING btree ("name");--> statement-breakpoint
CREATE INDEX "emex_vehicles_source_id_idx" ON "emex_vehicles" USING btree ("source_id");--> statement-breakpoint
CREATE INDEX "pcat_part_groups_catalog_id_idx" ON "pcat_part_groups" USING btree ("catalog_id");--> statement-breakpoint
CREATE INDEX "pcat_part_groups_parent_id_idx" ON "pcat_part_groups" USING btree ("parent_id");--> statement-breakpoint
CREATE INDEX "pcat_parts_name_idx" ON "pcat_parts" USING btree ("name");--> statement-breakpoint
CREATE INDEX "pcat_schema_pics_group_car_idx" ON "pcat_schema_pics" USING btree ("group_id","car_id");--> statement-breakpoint
CREATE INDEX "pcat_vehicles_vin_idx" ON "pcat_vehicles" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "pcat_vehicles_catalog_id_idx" ON "pcat_vehicles" USING btree ("catalog_id");--> statement-breakpoint
CREATE UNIQUE INDEX "pl24_catalogs_catalog_id_idx" ON "pl24_catalogs" USING btree ("catalog_id");--> statement-breakpoint
CREATE INDEX "pl24_part_groups_vehicle_id_idx" ON "pl24_part_groups" USING btree ("pl24_vehicle_id");--> statement-breakpoint
CREATE INDEX "pl24_part_groups_group_id_idx" ON "pl24_part_groups" USING btree ("group_id");--> statement-breakpoint
CREATE INDEX "pl24_part_images_part_id_idx" ON "pl24_part_images" USING btree ("pl24_part_id");--> statement-breakpoint
CREATE INDEX "pl24_part_numbers_part_id_idx" ON "pl24_part_numbers" USING btree ("pl24_part_id");--> statement-breakpoint
CREATE INDEX "pl24_part_numbers_oem_code_idx" ON "pl24_part_numbers" USING btree ("oem_code");--> statement-breakpoint
CREATE INDEX "pl24_parts_vehicle_id_idx" ON "pl24_parts" USING btree ("pl24_vehicle_id");--> statement-breakpoint
CREATE INDEX "pl24_parts_group_id_idx" ON "pl24_parts" USING btree ("group_id");--> statement-breakpoint
CREATE INDEX "pl24_schema_pics_group_id_idx" ON "pl24_schema_pics" USING btree ("group_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_groups_catalog_id_idx" ON "pl24_vehicle_groups" USING btree ("catalog_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_groups_group_id_idx" ON "pl24_vehicle_groups" USING btree ("group_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_parts_vehicle_id_idx" ON "pl24_vehicle_parts" USING btree ("pl24_vehicle_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_parts_part_id_idx" ON "pl24_vehicle_parts" USING btree ("pl24_part_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_vins_vin_idx" ON "pl24_vehicle_vins" USING btree ("vin");--> statement-breakpoint
CREATE INDEX "pl24_vehicle_vins_vehicle_id_idx" ON "pl24_vehicle_vins" USING btree ("pl24_vehicle_id");--> statement-breakpoint
CREATE UNIQUE INDEX "pl24_vehicles_vehicle_id_idx" ON "pl24_vehicles" USING btree ("vehicle_id");--> statement-breakpoint
CREATE INDEX "pl24_vehicles_catalog_id_idx" ON "pl24_vehicles" USING btree ("catalog_id");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1778535325959,
"tag": "0000_brief_guardian",
"breakpoints": true
}
]
}

View File

@@ -14,6 +14,8 @@
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:seed": "tsx src/database/seed.ts",
"db:migrate": "tsx src/database/migrate.ts",
"db:migrate:dist": "node dist/database/migrate.js",
"worker": "node dist/worker.js",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
@@ -52,6 +54,7 @@
"bullmq": "^5.30.0",
"bullmq-otel": "^1.2.0",
"dotenv": "^16.4.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.41.0",
"helmet": "^8.1.0",
"ioredis": "^5.4.0",
@@ -70,7 +73,6 @@
"@types/multer": "^1.4.0",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^3.2.4",
"drizzle-kit": "^0.31.4",
"playwright": "^1.50.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",

View File

@@ -0,0 +1,179 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { beforeEach, describe, expect, it, vi } from "vitest";
/**
* Unit tests for the migration runner's bootstrap logic.
*
* We test bootstrapExistingDb() directly with a mock postgres.Sql client
* and mock the filesystem via vi.mock("node:fs").
*/
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
return {
...actual,
readFileSync: vi.fn((path: string, _encoding: string) => {
// Default: no file — will cause errors unless overridden in test setup
throw new Error(`ENOENT: mock file not set up: ${path}`);
}),
};
});
import { bootstrapExistingDb } from "../../database/migrate";
// ── Helpers ──
interface MockSqlClient {
(queryParts: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
unsafe: ReturnType<typeof vi.fn>;
end: ReturnType<typeof vi.fn>;
__inserts: string[];
__unsafeCalls: string[];
}
function createMockSqlClient(): MockSqlClient {
let queryHandler: (template: string) => unknown[] = () => [];
const sql = vi.fn(async (queryParts: TemplateStringsArray, ..._values: unknown[]) => {
const queryText = queryParts.join(" ").trim();
// Capture INSERT queries directly
if (queryText.includes("INSERT INTO")) {
(sql as any).__inserts.push(queryText);
return [];
}
return queryHandler(queryText);
}) as unknown as MockSqlClient;
(sql as any).unsafe = vi.fn(async (q: string) => {
((sql as any).__unsafeCalls as string[]).push(q);
});
(sql as any).end = vi.fn().mockResolvedValue(undefined);
(sql as any).__inserts = [];
(sql as any).__unsafeCalls = [];
// Allow tests to set queryHandler
(sql as any)._setQueryHandler = (handler: typeof queryHandler) => {
queryHandler = handler;
};
return sql;
}
// ── Tests ──
describe("bootstrapExistingDb", () => {
let sqlClient: MockSqlClient;
beforeEach(() => {
vi.clearAllMocks();
sqlClient = createMockSqlClient();
});
describe("when __drizzle_migrations already exists", () => {
it("should skip bootstrap (no CREATE, no INSERT)", async () => {
(sqlClient as any)._setQueryHandler((queryText: string) => {
if (
queryText.includes("information_schema") &&
queryText.includes("__drizzle_migrations")
) {
return [{ table_name: "__drizzle_migrations" }];
}
return [];
});
await bootstrapExistingDb(sqlClient as any);
// Should NOT create schema or table
expect((sqlClient as any).__unsafeCalls).toEqual([]);
// Should NOT insert any migration records
expect((sqlClient as any).__inserts).toEqual([]);
});
});
describe("when __drizzle_migrations is missing but users exists (existing DB)", () => {
it("should create schema, create table, mark baseline entries as applied", async () => {
(sqlClient as any)._setQueryHandler((queryText: string) => {
if (
queryText.includes("information_schema") &&
queryText.includes("__drizzle_migrations")
) {
return []; // does not exist
}
if (queryText.includes("information_schema") && queryText.includes("'users'")) {
return [{ table_name: "users" }]; // users exist
}
return [];
});
// Set up the filesystem mocks
const mockRFS = readFileSync as ReturnType<typeof vi.fn>;
mockRFS.mockImplementation((path: string, _encoding: string) => {
if (path.includes("_journal.json")) {
return JSON.stringify({
version: "7",
dialect: "postgresql",
entries: [
{
idx: 0,
version: "7",
when: 1778535325959,
tag: "0000_brief_guardian",
breakpoints: true,
},
],
});
}
if (path.includes("0000_brief_guardian.sql")) {
return "CREATE TABLE foo";
}
throw new Error(`Unexpected readFileSync: ${path}`);
});
await bootstrapExistingDb(sqlClient as any);
// Verify CREATE SCHEMA was called
expect((sqlClient as any).__unsafeCalls).toContain('CREATE SCHEMA IF NOT EXISTS "drizzle"');
// Verify CREATE TABLE was called
const createTableCall = (sqlClient as any).__unsafeCalls.find((c: string) =>
c.includes("CREATE TABLE IF NOT EXISTS"),
);
expect(createTableCall).toBeDefined();
expect(createTableCall).toContain("__drizzle_migrations");
// Verify INSERT was called with a hash
expect((sqlClient as any).__inserts.length).toBeGreaterThanOrEqual(1);
});
});
describe("when both __drizzle_migrations and users are missing (fresh DB)", () => {
it("should skip bootstrap entirely", async () => {
(sqlClient as any)._setQueryHandler((_queryText: string) => {
return []; // nothing exists
});
await bootstrapExistingDb(sqlClient as any);
// No schema creation
expect((sqlClient as any).__unsafeCalls).toEqual([]);
// No inserts
expect((sqlClient as any).__inserts).toEqual([]);
});
});
});
describe("hash computation", () => {
it("should produce the expected SHA256 hash for a known input", () => {
const input = "SELECT 1";
const hash = createHash("sha256").update(input).digest("hex");
expect(hash).toBe("e004ebd5b5532a4b85984a62f8ad48a81aa3460c1ca07701f386135d72cdecf5");
});
it("should produce a different hash for different input", () => {
const hashA = createHash("sha256").update("SELECT 1").digest("hex");
const hashB = createHash("sha256").update("SELECT 2").digest("hex");
expect(hashA).not.toBe(hashB);
});
});

View File

@@ -0,0 +1,160 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
/**
* Path to the drizzle migrations folder.
*
* In development (tsx), __dirname = <repo>/apps/api/src/database,
* and the drizzle folder is at <repo>/apps/api/drizzle.
*
* In production (compiled), __dirname = <app>/apps/api/dist/database,
* and the drizzle folder is COPIED to <app>/apps/api/drizzle.
*
* Both resolve correctly with: join(__dirname, "../../drizzle")
*/
const migrationsFolder = process.env.MIGRATIONS_FOLDER || join(__dirname, "../../drizzle");
interface JournalEntry {
idx: number;
version: string;
when: number;
tag: string;
breakpoints: boolean;
}
interface Journal {
version: string;
dialect: string;
entries: JournalEntry[];
}
export async function bootstrapExistingDb(sqlClient: postgres.Sql): Promise<void> {
const tablesResult = await sqlClient`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'drizzle'
AND table_name = '__drizzle_migrations'
`;
if (tablesResult.length > 0) {
// __drizzle_migrations already exists — normal migration run
console.log("[migrate] Migration tracking table exists — skipping bootstrap");
return;
}
// Check if users table exists (canary for existing DB)
const usersResult = await sqlClient`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'users'
`;
if (usersResult.length === 0) {
// Fresh DB — migrate() will handle everything
console.log("[migrate] Fresh database detected — skipping bootstrap");
return;
}
// Existing DB with no migration tracking — bootstrap it
console.log("[migrate] Bootstrapping existing database...");
// Create the drizzle schema if it doesn't exist
await sqlClient.unsafe(`CREATE SCHEMA IF NOT EXISTS "drizzle"`);
// Create __drizzle_migrations table matching Drizzle's expected schema
await sqlClient.unsafe(`
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)
`);
// Read journal and mark baseline migrations as applied
const journalPath = join(migrationsFolder, "meta", "_journal.json");
let journal: Journal;
try {
journal = JSON.parse(readFileSync(journalPath, "utf-8")) as Journal;
} catch (err) {
console.error("[migrate] Failed to read _journal.json:", (err as Error).message);
throw err;
}
if (!journal.entries || journal.entries.length === 0) {
console.warn("[migrate] _journal.json has no entries — no migrations to bootstrap");
return;
}
const now = Date.now();
for (const entry of journal.entries) {
const sqlPath = join(migrationsFolder, `${entry.tag}.sql`);
let sqlContent: string;
try {
sqlContent = readFileSync(sqlPath, "utf-8");
} catch (err) {
console.error(`[migrate] Failed to read migration ${entry.tag}.sql:`, (err as Error).message);
throw err;
}
const hash = createHash("sha256").update(sqlContent).digest("hex");
await sqlClient`
INSERT INTO "drizzle"."__drizzle_migrations" (hash, created_at)
VALUES (${hash}, ${now})
`;
}
console.log(
`[migrate] Bootstrapped existing DB: marked ${journal.entries.length} baseline migration(s) as applied (no SQL executed)`,
);
}
async function run(): Promise<void> {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("[migrate] DATABASE_URL environment variable is required");
process.exit(1);
}
console.log(`[migrate] Migrations folder: ${migrationsFolder}`);
// Single connection for migration
const sqlClient = postgres(databaseUrl, { max: 1 });
try {
// Bootstrap existing databases before running the migrator
await bootstrapExistingDb(sqlClient);
// Run Drizzle migrator (idempotent — applies only unapplied migrations)
const db = drizzle(sqlClient);
await migrate(db, { migrationsFolder });
console.log("[migrate] Migration complete");
await sqlClient.end();
process.exit(0);
} catch (err) {
console.error("[migrate] Migration failed:", (err as Error).message);
try {
await sqlClient.end();
} catch {
// ignore close errors during failure
}
process.exit(1);
}
}
// Only auto-run when executed directly (not when imported in tests)
const isMainModule =
process.argv[1] &&
(process.argv[1].endsWith("migrate.ts") || process.argv[1].endsWith("migrate.js"));
if (isMainModule) {
run();
}

8
apps/api/start.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
echo "[start.sh] Running database migrations..."
node --enable-source-maps apps/api/dist/database/migrate.js
echo "[start.sh] Starting API server..."
exec node --enable-source-maps apps/api/dist/main.js

View File

@@ -354,6 +354,28 @@ catalogVehicles
- `emex_*` — EMEX scraper cache (similar structure with translations)
- `parts_catalogs_*` — PartsCatalogs API cache (defined in `parts-catalogs.ts`)
### Migration Workflow
**Strategy:** Formal Drizzle SQL migrations with automated startup.
**Schema change workflow:**
1. Edit schema files in `apps/api/src/database/schema/`
2. Run `pnpm db:generate` — produces a timestamped `.sql` file in `apps/api/drizzle/`
3. Review the SQL diff in PR (the migration is version-controlled alongside code)
4. Commit and deploy — migration runs automatically on container startup
**Container startup:** The API server runs `apps/api/start.sh` which executes `node dist/database/migrate.js` BEFORE starting the NestJS server. This ensures all pending migrations are applied before any code runs.
**Bootstrap note:** Existing databases with no migration history (`__drizzle_migrations` table missing) are automatically bootstrapped on first run. The bootstrap creates the tracking table and marks all baseline migrations as already applied (no SQL is executed against the live DB). This is safe for databases previously managed via `pnpm db:push`.
**Manual commands:**
- `pnpm db:migrate` — Run migrations in dev (uses `tsx` for direct TS execution)
- `pnpm db:migrate:dist` — Run migrations against compiled JS (for production manual override)
- `pnpm db:generate` — Create a new migration from schema changes
- `pnpm db:push` — Direct schema push (still available for local dev, discouraged for shared environments)
**Worker process:** The BullMQ worker overrides the Docker CMD in `docker-compose.coolify.yml` and does NOT run migrations. Only the API service handles migrations, preventing race conditions.
### Job Queues
**Framework:** BullMQ with Redis
@@ -752,7 +774,7 @@ Dependencies: Radix UI (accordion, dialog, dropdown-menu, label, popover, select
2. `git pull origin main`
3. `pnpm install`
4. `pnpm build`
5. `pnpm db:push` (migrations)
5. `pnpm db:migrate` (migrations)
6. PM2 reload all
### PM2 Configuration

6
pnpm-lock.yaml generated
View File

@@ -120,6 +120,9 @@ importers:
dotenv:
specifier: ^16.4.0
version: 16.4.5
drizzle-kit:
specifier: ^0.31.4
version: 0.31.9
drizzle-orm:
specifier: ^0.41.0
version: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(gel@2.2.0)(kysely@0.28.11)(postgres@3.4.8)
@@ -169,9 +172,6 @@ importers:
'@vitest/coverage-v8':
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
drizzle-kit:
specifier: ^0.31.4
version: 0.31.9
playwright:
specifier: ^1.50.0
version: 1.58.2