From c454d1a3fc4676c6eba5e7fa3a0641ccfca84e34 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 9 Jun 2026 06:45:04 +0200 Subject: [PATCH] feat(db): audit schema hardening (onDelete, unique doc-numbers, indexes) From the 2026-06-09 full-codebase audit. Warehouse line->header relations CASCADE on delete; line->master-data and the financial FKs are explicit RESTRICT; UNIQUE on receipt/issue/session numbers; new indexes on hot FK columns (sklad_issues/receipts, bank_accounts); dropped a duplicate audit_logs created_at index. Applied to dev `app` and `app_test`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migration.sql | 137 ++++++++++++++++++ prisma/schema.prisma | 77 +++++----- 2 files changed, 178 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20260609000000_audit_schema_hardening/migration.sql diff --git a/prisma/migrations/20260609000000_audit_schema_hardening/migration.sql b/prisma/migrations/20260609000000_audit_schema_hardening/migration.sql new file mode 100644 index 0000000..8a58124 --- /dev/null +++ b/prisma/migrations/20260609000000_audit_schema_hardening/migration.sql @@ -0,0 +1,137 @@ +-- Audit schema hardening (from the 2026-06-09 codebase audit). +-- +-- ⚠️ PENDING — NOT YET APPLIED. Review, then apply with the dev server stopped: +-- npx prisma migrate deploy (prod / already-tracked) +-- -- or, in dev, `npx prisma migrate dev` will pick it up as pending. +-- +-- What this does: +-- • Warehouse line→header relations now CASCADE on delete (deleting a +-- receipt/issue/inventory session removes its lines/attachments). +-- • Warehouse line→master-data (item/batch/location) and the financial FKs +-- (invoices/orders/projects/quotations → customer/order/quotation) are now +-- explicit ON DELETE RESTRICT — preventing deletion of a referenced master +-- record / orphaning of a financial record. (Some of these were previously +-- the optional-relation default SET NULL; RESTRICT is the safer intent.) +-- • UNIQUE on warehouse document numbers (receipt_number/issue_number/ +-- session_number) — like every other document-number column. +-- • New indexes on hot FK filters (sklad_issues/receipts, bank_accounts). +-- +-- ⚠️ BEFORE APPLYING ON PRODUCTION, verify existing data won't violate the new +-- constraints: no DUPLICATE non-null receipt/issue/session numbers (else the +-- UNIQUE index fails), and no rows that would break the RESTRICT FKs. Test on +-- a copy first. +-- +-- DropForeignKey +ALTER TABLE `invoices` DROP FOREIGN KEY `invoices_ibfk_1`; + +-- DropForeignKey +ALTER TABLE `invoices` DROP FOREIGN KEY `invoices_ibfk_2`; + +-- DropForeignKey +ALTER TABLE `orders` DROP FOREIGN KEY `orders_ibfk_1`; + +-- DropForeignKey +ALTER TABLE `orders` DROP FOREIGN KEY `orders_ibfk_2`; + +-- DropForeignKey +ALTER TABLE `projects` DROP FOREIGN KEY `projects_ibfk_1`; + +-- DropForeignKey +ALTER TABLE `projects` DROP FOREIGN KEY `projects_ibfk_2`; + +-- DropForeignKey +ALTER TABLE `projects` DROP FOREIGN KEY `projects_ibfk_3`; + +-- DropForeignKey +ALTER TABLE `quotations` DROP FOREIGN KEY `quotations_ibfk_1`; + +-- DropForeignKey +ALTER TABLE `sklad_receipt_lines` DROP FOREIGN KEY `sklad_receipt_lines_receipt_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_receipt_lines` DROP FOREIGN KEY `sklad_receipt_lines_location_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_receipt_attachments` DROP FOREIGN KEY `sklad_receipt_attachments_receipt_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_issue_lines` DROP FOREIGN KEY `sklad_issue_lines_issue_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_issue_lines` DROP FOREIGN KEY `sklad_issue_lines_location_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_inventory_lines` DROP FOREIGN KEY `sklad_inventory_lines_session_id_fkey`; + +-- DropForeignKey +ALTER TABLE `sklad_inventory_lines` DROP FOREIGN KEY `sklad_inventory_lines_location_id_fkey`; + +-- CreateIndex +CREATE INDEX `idx_bank_accounts_is_default` ON `bank_accounts`(`is_default`); + +-- CreateIndex +CREATE UNIQUE INDEX `sklad_receipts_receipt_number_key` ON `sklad_receipts`(`receipt_number`); + +-- CreateIndex +CREATE INDEX `sklad_receipts_supplier_id` ON `sklad_receipts`(`supplier_id`); + +-- CreateIndex +CREATE INDEX `sklad_receipts_received_by` ON `sklad_receipts`(`received_by`); + +-- CreateIndex +CREATE UNIQUE INDEX `sklad_issues_issue_number_key` ON `sklad_issues`(`issue_number`); + +-- CreateIndex +CREATE INDEX `sklad_issues_project_id` ON `sklad_issues`(`project_id`); + +-- CreateIndex +CREATE INDEX `sklad_issues_issued_by` ON `sklad_issues`(`issued_by`); + +-- CreateIndex +CREATE UNIQUE INDEX `sklad_inventory_sessions_session_number_key` ON `sklad_inventory_sessions`(`session_number`); + +-- AddForeignKey +ALTER TABLE `invoices` ADD CONSTRAINT `invoices_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `invoices` ADD CONSTRAINT `invoices_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `orders` ADD CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`quotation_id`) REFERENCES `quotations`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `orders` ADD CONSTRAINT `orders_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `projects` ADD CONSTRAINT `projects_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `projects` ADD CONSTRAINT `projects_ibfk_2` FOREIGN KEY (`quotation_id`) REFERENCES `quotations`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `projects` ADD CONSTRAINT `projects_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `quotations` ADD CONSTRAINT `quotations_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE `sklad_receipt_lines` ADD CONSTRAINT `sklad_receipt_lines_receipt_id_fkey` FOREIGN KEY (`receipt_id`) REFERENCES `sklad_receipts`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_receipt_lines` ADD CONSTRAINT `sklad_receipt_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_receipt_attachments` ADD CONSTRAINT `sklad_receipt_attachments_receipt_id_fkey` FOREIGN KEY (`receipt_id`) REFERENCES `sklad_receipts`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_issue_id_fkey` FOREIGN KEY (`issue_id`) REFERENCES `sklad_issues`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_issue_lines` ADD CONSTRAINT `sklad_issue_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_inventory_lines` ADD CONSTRAINT `sklad_inventory_lines_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `sklad_inventory_sessions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `sklad_inventory_lines` ADD CONSTRAINT `sklad_inventory_lines_location_id_fkey` FOREIGN KEY (`location_id`) REFERENCES `sklad_locations`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7128579..f0f27f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -68,7 +68,6 @@ model audit_logs { session_id String? @db.VarChar(128) created_at DateTime? @default(now()) @db.Timestamp(0) - @@index([created_at], map: "idx_audit_log_created") @@index([action], map: "idx_audit_logs_action") @@index([created_at], map: "idx_audit_logs_created") @@index([entity_type, entity_id, created_at], map: "idx_audit_logs_entity") @@ -124,6 +123,8 @@ model bank_accounts { position Int? @default(0) created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) + + @@index([is_default], map: "idx_bank_accounts_is_default") } model company_settings { @@ -235,8 +236,8 @@ model invoices { created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) invoice_items invoice_items[] - orders orders? @relation(fields: [order_id], references: [id], onUpdate: NoAction, map: "invoices_ibfk_1") - customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "invoices_ibfk_2") + orders orders? @relation(fields: [order_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "invoices_ibfk_1") + customers customers? @relation(fields: [customer_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "invoices_ibfk_2") @@index([customer_id], map: "customer_id") @@index([due_date], map: "idx_invoices_due_date") @@ -354,8 +355,8 @@ model orders { invoices invoices[] order_items order_items[] order_sections order_sections[] - quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "orders_ibfk_1") - customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "orders_ibfk_2") + quotations quotations? @relation(fields: [quotation_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "orders_ibfk_1") + customers customers? @relation(fields: [customer_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "orders_ibfk_2") projects projects[] @@index([customer_id], map: "customer_id") @@ -405,9 +406,9 @@ model projects { work_plan_entries work_plan_entries[] work_plan_overrides work_plan_overrides[] users users? @relation(fields: [responsible_user_id], references: [id], onUpdate: NoAction, map: "fk_projects_responsible_user") - customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_1") - quotations quotations? @relation(fields: [quotation_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_2") - orders orders? @relation(fields: [order_id], references: [id], onUpdate: NoAction, map: "projects_ibfk_3") + customers customers? @relation(fields: [customer_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "projects_ibfk_1") + quotations quotations? @relation(fields: [quotation_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "projects_ibfk_2") + orders orders? @relation(fields: [order_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "projects_ibfk_3") @@index([customer_id], map: "customer_id") @@index([responsible_user_id], map: "fk_projects_responsible_user") @@ -452,7 +453,7 @@ model quotations { orders orders[] projects projects[] quotation_items quotation_items[] - customers customers? @relation(fields: [customer_id], references: [id], onUpdate: NoAction, map: "quotations_ibfk_1") + customers customers? @relation(fields: [customer_id], references: [id], onDelete: Restrict, onUpdate: NoAction, map: "quotations_ibfk_1") scope_sections scope_sections[] @@index([customer_id], map: "customer_id") @@ -777,7 +778,7 @@ model sklad_items { created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) - category sklad_categories? @relation(fields: [category_id], references: [id]) + category sklad_categories? @relation(fields: [category_id], references: [id], onDelete: SetNull) batches sklad_batches[] item_locations sklad_item_locations[] receipt_lines sklad_receipt_lines[] @@ -799,8 +800,8 @@ model sklad_batches { received_at DateTime? @db.DateTime(0) is_consumed Boolean @default(false) - item sklad_items @relation(fields: [item_id], references: [id]) - receipt_line sklad_receipt_lines @relation(fields: [receipt_line_id], references: [id]) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + receipt_line sklad_receipt_lines @relation(fields: [receipt_line_id], references: [id], onDelete: Restrict) issue_lines sklad_issue_lines[] @@index([item_id, is_consumed, received_at], map: "sklad_batches_fifo") @@ -813,8 +814,8 @@ model sklad_item_locations { location_id Int quantity Decimal @default(0) @db.Decimal(12, 3) - item sklad_items @relation(fields: [item_id], references: [id]) - location sklad_locations @relation(fields: [location_id], references: [id]) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + location sklad_locations @relation(fields: [location_id], references: [id], onDelete: Restrict) @@unique([item_id, location_id], map: "sklad_item_locations_unique") @@map("sklad_item_locations") @@ -822,7 +823,7 @@ model sklad_item_locations { model sklad_receipts { id Int @id @default(autoincrement()) - receipt_number String? @db.VarChar(50) + receipt_number String? @unique @db.VarChar(50) supplier_id Int? delivery_note_number String? @db.VarChar(100) delivery_note_date DateTime? @db.DateTime(0) @@ -832,11 +833,13 @@ model sklad_receipts { created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) - supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id]) - received_by_user users? @relation("SkladReceivedBy", fields: [received_by], references: [id]) + supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id], onDelete: SetNull) + received_by_user users? @relation("SkladReceivedBy", fields: [received_by], references: [id], onDelete: SetNull) items sklad_receipt_lines[] attachments sklad_receipt_attachments[] + @@index([supplier_id], map: "sklad_receipts_supplier_id") + @@index([received_by], map: "sklad_receipts_received_by") @@map("sklad_receipts") } @@ -849,9 +852,9 @@ model sklad_receipt_lines { location_id Int? notes String? @db.VarChar(255) - receipt sklad_receipts @relation(fields: [receipt_id], references: [id]) - item sklad_items @relation(fields: [item_id], references: [id]) - location sklad_locations? @relation(fields: [location_id], references: [id]) + receipt sklad_receipts @relation(fields: [receipt_id], references: [id], onDelete: Cascade) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + location sklad_locations? @relation(fields: [location_id], references: [id], onDelete: Restrict) batch sklad_batches? @@index([receipt_id], map: "sklad_receipt_lines_receipt_id") @@ -867,14 +870,14 @@ model sklad_receipt_attachments { file_path String @db.VarChar(500) created_at DateTime? @default(now()) @db.DateTime(0) - receipt sklad_receipts @relation(fields: [receipt_id], references: [id]) + receipt sklad_receipts @relation(fields: [receipt_id], references: [id], onDelete: Cascade) @@map("sklad_receipt_attachments") } model sklad_issues { id Int @id @default(autoincrement()) - issue_number String? @db.VarChar(50) + issue_number String? @unique @db.VarChar(50) project_id Int issued_by Int? notes String? @db.Text @@ -882,10 +885,12 @@ model sklad_issues { created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) - project projects @relation(fields: [project_id], references: [id]) - issued_by_user users? @relation("SkladIssuedBy", fields: [issued_by], references: [id]) + project projects @relation(fields: [project_id], references: [id], onDelete: Restrict) + issued_by_user users? @relation("SkladIssuedBy", fields: [issued_by], references: [id], onDelete: SetNull) items sklad_issue_lines[] + @@index([project_id], map: "sklad_issues_project_id") + @@index([issued_by], map: "sklad_issues_issued_by") @@map("sklad_issues") } @@ -899,11 +904,11 @@ model sklad_issue_lines { reservation_id Int? notes String? @db.VarChar(255) - issue sklad_issues @relation(fields: [issue_id], references: [id]) - item sklad_items @relation(fields: [item_id], references: [id]) - batch sklad_batches @relation(fields: [batch_id], references: [id]) - location sklad_locations? @relation(fields: [location_id], references: [id]) - reservation sklad_reservations? @relation(fields: [reservation_id], references: [id]) + issue sklad_issues @relation(fields: [issue_id], references: [id], onDelete: Cascade) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + batch sklad_batches @relation(fields: [batch_id], references: [id], onDelete: Restrict) + location sklad_locations? @relation(fields: [location_id], references: [id], onDelete: Restrict) + reservation sklad_reservations? @relation(fields: [reservation_id], references: [id], onDelete: SetNull) @@index([issue_id], map: "sklad_issue_lines_issue_id") @@map("sklad_issue_lines") @@ -921,9 +926,9 @@ model sklad_reservations { created_at DateTime? @default(now()) @db.DateTime(0) modified_at DateTime? @db.DateTime(0) - item sklad_items @relation(fields: [item_id], references: [id]) - project projects @relation(fields: [project_id], references: [id]) - reserved_by_user users? @relation("SkladReservedBy", fields: [reserved_by], references: [id]) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + project projects @relation(fields: [project_id], references: [id], onDelete: Restrict) + reserved_by_user users? @relation("SkladReservedBy", fields: [reserved_by], references: [id], onDelete: SetNull) issue_lines sklad_issue_lines[] @@index([item_id, status], map: "sklad_reservations_item_status") @@ -932,7 +937,7 @@ model sklad_reservations { model sklad_inventory_sessions { id Int @id @default(autoincrement()) - session_number String? @db.VarChar(50) + session_number String? @unique @db.VarChar(50) notes String? @db.Text status sklad_inventory_status @default(DRAFT) created_at DateTime? @default(now()) @db.DateTime(0) @@ -953,9 +958,9 @@ model sklad_inventory_lines { difference Decimal @db.Decimal(12, 3) notes String? @db.VarChar(255) - session sklad_inventory_sessions @relation(fields: [session_id], references: [id]) - item sklad_items @relation(fields: [item_id], references: [id]) - location sklad_locations? @relation(fields: [location_id], references: [id]) + session sklad_inventory_sessions @relation(fields: [session_id], references: [id], onDelete: Cascade) + item sklad_items @relation(fields: [item_id], references: [id], onDelete: Restrict) + location sklad_locations? @relation(fields: [location_id], references: [id], onDelete: Restrict) @@index([session_id], map: "sklad_inventory_lines_session_id") @@map("sklad_inventory_lines")