diff --git a/apps/backend/drizzle/0000_clammy_invisible_woman.sql b/apps/backend/drizzle/0000_nostalgic_ben_parker.sql similarity index 75% rename from apps/backend/drizzle/0000_clammy_invisible_woman.sql rename to apps/backend/drizzle/0000_nostalgic_ben_parker.sql index 6a62fb91..98e606fe 100644 --- a/apps/backend/drizzle/0000_clammy_invisible_woman.sql +++ b/apps/backend/drizzle/0000_nostalgic_ben_parker.sql @@ -12,10 +12,11 @@ CREATE TABLE `accounts` ( `type` text NOT NULL, `updated_at` text NOT NULL, `user_id` text NOT NULL, - FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`,`commodity_id`) REFERENCES `commodities`(`user_id`,`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint +CREATE UNIQUE INDEX `accounts_user_id_id_unique_idx` ON `accounts` (`user_id`,`id`);--> statement-breakpoint CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`);--> statement-breakpoint CREATE TABLE `transactions` ( `commodity_id` text NOT NULL, @@ -28,11 +29,12 @@ CREATE TABLE `transactions` ( `updated_at` text NOT NULL, `user_id` text NOT NULL, `version` integer NOT NULL, - FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`,`commodity_id`) REFERENCES `commodities`(`user_id`,`id`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint CREATE INDEX `idx_transactions_user_date` ON `transactions` (`user_id`,`transaction_date`);--> statement-breakpoint +CREATE UNIQUE INDEX `transactions_user_id_id_unique_idx` ON `transactions` (`user_id`,`id`);--> statement-breakpoint CREATE TABLE `operations` ( `account_id` text NOT NULL, `amount` text NOT NULL, @@ -45,9 +47,9 @@ CREATE TABLE `operations` ( `updated_at` text NOT NULL, `user_id` text NOT NULL, `value` text NOT NULL, - FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE restrict, - FOREIGN KEY (`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`user_id`,`account_id`) REFERENCES `accounts`(`user_id`,`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`user_id`,`transaction_id`) REFERENCES `transactions`(`user_id`,`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE INDEX `idx_operations_transaction` ON `operations` (`transaction_id`);--> statement-breakpoint @@ -74,6 +76,7 @@ CREATE TABLE `commodities` ( `code` text NOT NULL, `created_at` text NOT NULL, `id` text PRIMARY KEY NOT NULL, + `is_closed` integer NOT NULL, `is_tombstone` integer NOT NULL, `name` text NOT NULL, `precision` integer NOT NULL, @@ -83,4 +86,5 @@ CREATE TABLE `commodities` ( FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint -CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`); \ No newline at end of file +CREATE UNIQUE INDEX `commodities_user_id_code_unique_idx` ON `commodities` (`user_id`,`code`);--> statement-breakpoint +CREATE UNIQUE INDEX `commodities_user_id_id_unique_idx` ON `commodities` (`user_id`,`id`); \ No newline at end of file diff --git a/apps/backend/drizzle/0001_adorable_star_brand.sql b/apps/backend/drizzle/0001_adorable_star_brand.sql deleted file mode 100644 index 60a3fb4d..00000000 --- a/apps/backend/drizzle/0001_adorable_star_brand.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `commodities` ADD `is_closed` integer NOT NULL; \ No newline at end of file diff --git a/apps/backend/drizzle/0002_numerous_alice.sql b/apps/backend/drizzle/0002_numerous_alice.sql deleted file mode 100644 index a62a25bc..00000000 --- a/apps/backend/drizzle/0002_numerous_alice.sql +++ /dev/null @@ -1,26 +0,0 @@ -CREATE UNIQUE INDEX `commodities_user_id_id_unique_idx` ON `commodities` (`user_id`,`id`);--> statement-breakpoint -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_accounts` ( - `commodity_id` text NOT NULL, - `created_at` text NOT NULL, - `current_cleared_balance_local` text NOT NULL, - `description` text NOT NULL, - `id` text PRIMARY KEY NOT NULL, - `initial_balance` text NOT NULL, - `is_closed` integer NOT NULL, - `is_system` integer NOT NULL, - `is_tombstone` integer NOT NULL, - `name` text NOT NULL, - `type` text NOT NULL, - `updated_at` text NOT NULL, - `user_id` text NOT NULL, - FOREIGN KEY (`commodity_id`) REFERENCES `commodities`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`user_id`,`commodity_id`) REFERENCES `commodities`(`user_id`,`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_accounts`("commodity_id", "created_at", "current_cleared_balance_local", "description", "id", "initial_balance", "is_closed", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id") SELECT "commodity_id", "created_at", "current_cleared_balance_local", "description", "id", "initial_balance", "is_closed", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id" FROM `accounts`;--> statement-breakpoint -DROP TABLE `accounts`;--> statement-breakpoint -ALTER TABLE `__new_accounts` RENAME TO `accounts`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`); \ No newline at end of file diff --git a/apps/backend/drizzle/0003_bored_daredevil.sql b/apps/backend/drizzle/0003_bored_daredevil.sql deleted file mode 100644 index 5c3172b7..00000000 --- a/apps/backend/drizzle/0003_bored_daredevil.sql +++ /dev/null @@ -1,24 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_accounts` ( - `commodity_id` text NOT NULL, - `created_at` text NOT NULL, - `current_cleared_balance_local` text NOT NULL, - `description` text NOT NULL, - `id` text PRIMARY KEY NOT NULL, - `initial_balance` text NOT NULL, - `is_closed` integer NOT NULL, - `is_system` integer NOT NULL, - `is_tombstone` integer NOT NULL, - `name` text NOT NULL, - `type` text NOT NULL, - `updated_at` text NOT NULL, - `user_id` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`user_id`,`commodity_id`) REFERENCES `commodities`(`user_id`,`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -INSERT INTO `__new_accounts`("commodity_id", "created_at", "current_cleared_balance_local", "description", "id", "initial_balance", "is_closed", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id") SELECT "commodity_id", "created_at", "current_cleared_balance_local", "description", "id", "initial_balance", "is_closed", "is_system", "is_tombstone", "name", "type", "updated_at", "user_id" FROM `accounts`;--> statement-breakpoint -DROP TABLE `accounts`;--> statement-breakpoint -ALTER TABLE `__new_accounts` RENAME TO `accounts`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE UNIQUE INDEX `user_id_name_unique_idx` ON `accounts` (`user_id`,`name`); \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0000_snapshot.json b/apps/backend/drizzle/meta/0000_snapshot.json index ba900993..6919cde4 100644 --- a/apps/backend/drizzle/meta/0000_snapshot.json +++ b/apps/backend/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "72b37ad6-4dd9-43c4-8017-7b838aee4e7b", + "id": "ad990ec5-37cd-4951-b167-95daaec0d35f", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "accounts": { @@ -100,6 +100,14 @@ } }, "indexes": { + "accounts_user_id_id_unique_idx": { + "name": "accounts_user_id_id_unique_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": true + }, "user_id_name_unique_idx": { "name": "user_id_name_unique_idx", "columns": [ @@ -110,30 +118,32 @@ } }, "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", "tableFrom": "accounts", - "tableTo": "commodities", + "tableTo": "users", "columnsFrom": [ - "commodity_id" + "user_id" ], "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", + "accounts_user_id_commodity_id_commodities_user_id_id_fk": { + "name": "accounts_user_id_commodity_id_commodities_user_id_id_fk", "tableFrom": "accounts", - "tableTo": "users", + "tableTo": "commodities", "columnsFrom": [ - "user_id" + "user_id", + "commodity_id" ], "columnsTo": [ + "user_id", "id" ], - "onDelete": "cascade", + "onDelete": "no action", "onUpdate": "no action" } }, @@ -223,33 +233,43 @@ "transaction_date" ], "isUnique": false + }, + "transactions_user_id_id_unique_idx": { + "name": "transactions_user_id_id_unique_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": true } }, "foreignKeys": { - "transactions_commodity_id_commodities_id_fk": { - "name": "transactions_commodity_id_commodities_id_fk", + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", "tableFrom": "transactions", - "tableTo": "commodities", + "tableTo": "users", "columnsFrom": [ - "commodity_id" + "user_id" ], "columnsTo": [ "id" ], - "onDelete": "no action", + "onDelete": "cascade", "onUpdate": "no action" }, - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", + "transactions_user_id_commodity_id_commodities_user_id_id_fk": { + "name": "transactions_user_id_commodity_id_commodities_user_id_id_fk", "tableFrom": "transactions", - "tableTo": "users", + "tableTo": "commodities", "columnsFrom": [ - "user_id" + "user_id", + "commodity_id" ], "columnsTo": [ + "user_id", "id" ], - "onDelete": "cascade", + "onDelete": "no action", "onUpdate": "no action" } }, @@ -362,40 +382,44 @@ } }, "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", + "operations_user_id_users_id_fk": { + "name": "operations_user_id_users_id_fk", "tableFrom": "operations", - "tableTo": "accounts", + "tableTo": "users", "columnsFrom": [ - "account_id" + "user_id" ], "columnsTo": [ "id" ], - "onDelete": "restrict", + "onDelete": "cascade", "onUpdate": "no action" }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", + "operations_user_id_account_id_accounts_user_id_id_fk": { + "name": "operations_user_id_account_id_accounts_user_id_id_fk", "tableFrom": "operations", - "tableTo": "transactions", + "tableTo": "accounts", "columnsFrom": [ - "transaction_id" + "user_id", + "account_id" ], "columnsTo": [ + "user_id", "id" ], - "onDelete": "cascade", + "onDelete": "restrict", "onUpdate": "no action" }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", + "operations_user_id_transaction_id_transactions_user_id_id_fk": { + "name": "operations_user_id_transaction_id_transactions_user_id_id_fk", "tableFrom": "operations", - "tableTo": "users", + "tableTo": "transactions", "columnsFrom": [ - "user_id" + "user_id", + "transaction_id" ], "columnsTo": [ + "user_id", "id" ], "onDelete": "cascade", @@ -535,6 +559,13 @@ "notNull": true, "autoincrement": false }, + "is_closed": { + "name": "is_closed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, "is_tombstone": { "name": "is_tombstone", "type": "integer", @@ -586,6 +617,14 @@ "code" ], "isUnique": true + }, + "commodities_user_id_id_unique_idx": { + "name": "commodities_user_id_id_unique_idx", + "columns": [ + "user_id", + "id" + ], + "isUnique": true } }, "foreignKeys": { diff --git a/apps/backend/drizzle/meta/0001_snapshot.json b/apps/backend/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 11b7f8f9..00000000 --- a/apps/backend/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,628 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "e80e6eea-4e83-4ffe-a8d0-8d3a50183d1b", - "prevId": "72b37ad6-4dd9-43c4-8017-7b838aee4e7b", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "current_cleared_balance_local": { - "name": "current_cleared_balance_local", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_commodity_id_commodities_id_fk": { - "name": "transactions_commodity_id_commodities_id_fk", - "tableFrom": "transactions", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "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": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0002_snapshot.json b/apps/backend/drizzle/meta/0002_snapshot.json deleted file mode 100644 index 2b7b6dbe..00000000 --- a/apps/backend/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,651 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "765211f5-980b-4844-961e-490a5746c9d5", - "prevId": "e80e6eea-4e83-4ffe-a8d0-8d3a50183d1b", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "current_cleared_balance_local": { - "name": "current_cleared_balance_local", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_commodity_id_commodities_id_fk": { - "name": "accounts_commodity_id_commodities_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "accounts_user_id_commodity_id_commodities_user_id_id_fk": { - "name": "accounts_user_id_commodity_id_commodities_user_id_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "user_id", - "commodity_id" - ], - "columnsTo": [ - "user_id", - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_commodity_id_commodities_id_fk": { - "name": "transactions_commodity_id_commodities_id_fk", - "tableFrom": "transactions", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "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": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - }, - "commodities_user_id_id_unique_idx": { - "name": "commodities_user_id_id_unique_idx", - "columns": [ - "user_id", - "id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0003_snapshot.json b/apps/backend/drizzle/meta/0003_snapshot.json deleted file mode 100644 index cafcc749..00000000 --- a/apps/backend/drizzle/meta/0003_snapshot.json +++ /dev/null @@ -1,638 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "ad45c791-cf15-40f1-b7db-8c953a976d82", - "prevId": "765211f5-980b-4844-961e-490a5746c9d5", - "tables": { - "accounts": { - "name": "accounts", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "current_cleared_balance_local": { - "name": "current_cleared_balance_local", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "initial_balance": { - "name": "initial_balance", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_id_name_unique_idx": { - "name": "user_id_name_unique_idx", - "columns": [ - "user_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "accounts_user_id_users_id_fk": { - "name": "accounts_user_id_users_id_fk", - "tableFrom": "accounts", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "accounts_user_id_commodity_id_commodities_user_id_id_fk": { - "name": "accounts_user_id_commodity_id_commodities_user_id_id_fk", - "tableFrom": "accounts", - "tableTo": "commodities", - "columnsFrom": [ - "user_id", - "commodity_id" - ], - "columnsTo": [ - "user_id", - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "transactions": { - "name": "transactions", - "columns": { - "commodity_id": { - "name": "commodity_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "posting_date": { - "name": "posting_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_date": { - "name": "transaction_date", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_transactions_user_date": { - "name": "idx_transactions_user_date", - "columns": [ - "user_id", - "transaction_date" - ], - "isUnique": false - } - }, - "foreignKeys": { - "transactions_commodity_id_commodities_id_fk": { - "name": "transactions_commodity_id_commodities_id_fk", - "tableFrom": "transactions", - "tableTo": "commodities", - "columnsFrom": [ - "commodity_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "transactions_user_id_users_id_fk": { - "name": "transactions_user_id_users_id_fk", - "tableFrom": "transactions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "operations": { - "name": "operations", - "columns": { - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "amount": { - "name": "amount", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_system": { - "name": "is_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "transaction_id": { - "name": "transaction_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "idx_operations_transaction": { - "name": "idx_operations_transaction", - "columns": [ - "transaction_id" - ], - "isUnique": false - }, - "idx_operations_account": { - "name": "idx_operations_account", - "columns": [ - "account_id" - ], - "isUnique": false - }, - "idx_operations_user": { - "name": "idx_operations_user", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "operations_account_id_accounts_id_fk": { - "name": "operations_account_id_accounts_id_fk", - "tableFrom": "operations", - "tableTo": "accounts", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "operations_transaction_id_transactions_id_fk": { - "name": "operations_transaction_id_transactions_id_fk", - "tableFrom": "operations", - "tableTo": "transactions", - "columnsFrom": [ - "transaction_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "operations_user_id_users_id_fk": { - "name": "operations_user_id_users_id_fk", - "tableFrom": "operations", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users": { - "name": "users", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_email_unique": { - "name": "users_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "settings": { - "name": "settings", - "columns": { - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "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": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "commodities": { - "name": "commodities", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "is_closed": { - "name": "is_closed", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_tombstone": { - "name": "is_tombstone", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "precision": { - "name": "precision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "symbol": { - "name": "symbol", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "commodities_user_id_code_unique_idx": { - "name": "commodities_user_id_code_unique_idx", - "columns": [ - "user_id", - "code" - ], - "isUnique": true - }, - "commodities_user_id_id_unique_idx": { - "name": "commodities_user_id_id_unique_idx", - "columns": [ - "user_id", - "id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "commodities_user_id_users_id_fk": { - "name": "commodities_user_id_users_id_fk", - "tableFrom": "commodities", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index e05cc786..7721477d 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -5,29 +5,8 @@ { "idx": 0, "version": "6", - "when": 1785508223175, - "tag": "0000_clammy_invisible_woman", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1785859950401, - "tag": "0001_adorable_star_brand", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1785931823375, - "tag": "0002_numerous_alice", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1785935002154, - "tag": "0003_bored_daredevil", + "when": 1786004474849, + "tag": "0000_nostalgic_ben_parker", "breakpoints": true } ] diff --git a/apps/backend/src/application/services/CommodityReferencePolicy/__tests__/commodity-reference.policy.test.ts b/apps/backend/src/application/services/CommodityReferencePolicy/__tests__/commodity-reference.policy.test.ts index 0fb79caf..e0da1017 100644 --- a/apps/backend/src/application/services/CommodityReferencePolicy/__tests__/commodity-reference.policy.test.ts +++ b/apps/backend/src/application/services/CommodityReferencePolicy/__tests__/commodity-reference.policy.test.ts @@ -12,7 +12,6 @@ import { CommodityReferencePolicy } from 'src/application/services/CommodityRefe import { createUser } from 'src/db/createTestUser'; import { Commodity } from 'src/domain/commodities'; import { CommodityCode, Id } from 'src/domain/domain-core'; -import { DeletedEntityOperationError } from 'src/domain/domain.errors'; import { beforeEach, describe, expect, it, vi } from 'vitest'; describe('CommodityReferencePolicy', async () => { @@ -89,27 +88,6 @@ describe('CommodityReferencePolicy', async () => { }); }); - it('rejects deleted commodities for new account references', async () => { - const commodity = createCommodity(); - commodity.delete(); - - commodityRepository.getById.mockResolvedValueOnce({ - ...commodity.toSnapshot(), - id: commodityId, - }); - - const assertion = policy.assertUsableForNewAccount(userId, commodityId); - - await expect(assertion).rejects.toThrowError(DeletedEntityOperationError); - await expect(assertion).rejects.toMatchObject({ - code: apiErrorCodes.deletedEntityOperation, - context: { - entityType: Commodity.entityType, - operation: 'use', - }, - }); - }); - it('propagates repository lookup errors', async () => { const repositoryError = new Error('repository unavailable'); diff --git a/apps/backend/src/application/services/CommodityReferencePolicy/commodity-reference.policy.ts b/apps/backend/src/application/services/CommodityReferencePolicy/commodity-reference.policy.ts index 8966e84e..a9dac0f7 100644 --- a/apps/backend/src/application/services/CommodityReferencePolicy/commodity-reference.policy.ts +++ b/apps/backend/src/application/services/CommodityReferencePolicy/commodity-reference.policy.ts @@ -9,7 +9,6 @@ import type { TransactionRepositoryInterface, } from 'src/application/interfaces'; import { Commodity } from 'src/domain/commodities'; -import { DeletedEntityOperationError } from 'src/domain/domain.errors'; export class CommodityReferencePolicy { constructor( @@ -29,10 +28,6 @@ export class CommodityReferencePolicy { const commodity = Commodity.restore(commoditySnapshot); - if (commodity.isDeleted()) { - throw DeletedEntityOperationError.forUse(Commodity.entityType); - } - if (commodity.closed) { throw new CommodityClosedError(commodityId, 'create_account'); } diff --git a/apps/backend/src/db/referential-integrity.test.ts b/apps/backend/src/db/referential-integrity.test.ts new file mode 100644 index 00000000..b2cbfe97 --- /dev/null +++ b/apps/backend/src/db/referential-integrity.test.ts @@ -0,0 +1,60 @@ +import { BaseRepository, TransactionManager } from 'src/infrastructure/db'; +import { + DBErrorContext, + ForeignKeyConstraintError, +} from 'src/infrastructure/errors'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { TestDB } from './test-db'; + +class TestDbOperationExecutor extends BaseRepository { + run(operation: () => Promise, context: DBErrorContext): Promise { + return this.executeDatabaseOperation( + operation, + 'Database referential integrity check', + context, + ); + } +} + +describe('Database referential integrity', () => { + let testDB: TestDB; + let executor: TestDbOperationExecutor; + + beforeEach(async () => { + testDB = new TestDB(); + await testDB.setupTestDb(); + + executor = new TestDbOperationExecutor({ + getCurrentTransaction: () => testDB.db, + run: async (callback) => callback(), + } as TransactionManager); + }); + + afterEach(async () => { + await testDB.cleanupTestDb(); + }); + + it('rejects transaction row insert when commodity belongs to another user', async () => { + const user = await testDB.createUser({ + email: 'user@example.com', + name: 'User', + }); + const otherUser = await testDB.createUser({ + email: 'other-user@example.com', + name: 'Other User', + }); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); + + await expect( + executor.run( + () => testDB.createTransaction(user.id, otherUserCommodity.id), + { + field: 'commodityId', + tableName: 'transactions', + value: otherUserCommodity.id, + }, + ), + ).rejects.toThrow(ForeignKeyConstraintError); + }); +}); diff --git a/apps/backend/src/db/schemas/accounts.ts b/apps/backend/src/db/schemas/accounts.ts index 165a7a53..7a39375f 100644 --- a/apps/backend/src/db/schemas/accounts.ts +++ b/apps/backend/src/db/schemas/accounts.ts @@ -51,6 +51,7 @@ export const accountsTable = sqliteTable( foreignColumns: [commoditiesTable.userId, commoditiesTable.id], name: 'accounts_user_id_commodity_id_commodities_user_id_id_fk', }), + uniqueIndex('accounts_user_id_id_unique_idx').on(table.userId, table.id), uniqueIndex('user_id_name_unique_idx').on(table.userId, table.name), ], ); diff --git a/apps/backend/src/db/schemas/operations.ts b/apps/backend/src/db/schemas/operations.ts index 07fba6f0..5f987d48 100644 --- a/apps/backend/src/db/schemas/operations.ts +++ b/apps/backend/src/db/schemas/operations.ts @@ -1,6 +1,6 @@ import { UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm'; -import { sqliteTable, text, index } from 'drizzle-orm/sqlite-core'; +import { foreignKey, index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { accountsTable } from './accounts'; import { @@ -18,20 +18,14 @@ import { usersTable } from './users'; export const operationsTable = sqliteTable( 'operations', { - accountId: text('account_id') - .notNull() - .references(() => accountsTable.id, { onDelete: 'restrict' }) - .$type(), + accountId: text('account_id').notNull().$type(), amount: getAmountColumn('amount'), createdAt, description, id, isSystem, isTombstone, - transactionId: text('transaction_id') - .notNull() - .references(() => transactionsTable.id, { onDelete: 'cascade' }) - .$type(), + transactionId: text('transaction_id').notNull().$type(), updatedAt, userId: text('user_id') .notNull() @@ -39,18 +33,28 @@ export const operationsTable = sqliteTable( .$type(), value: getAmountColumn('value'), }, - (t) => [ - index('idx_operations_transaction').on(t.transactionId), - index('idx_operations_account').on(t.accountId), - index('idx_operations_user').on(t.userId), + (table) => [ + foreignKey({ + columns: [table.userId, table.accountId], + foreignColumns: [accountsTable.userId, accountsTable.id], + name: 'operations_user_id_account_id_accounts_user_id_id_fk', + }).onDelete('restrict'), + foreignKey({ + columns: [table.userId, table.transactionId], + foreignColumns: [transactionsTable.userId, transactionsTable.id], + name: 'operations_user_id_transaction_id_transactions_user_id_id_fk', + }).onDelete('cascade'), + index('idx_operations_transaction').on(table.transactionId), + index('idx_operations_account').on(table.accountId), + index('idx_operations_user').on(table.userId), ], ); // TODO: check if relations are needed for operations, or if they can be accessed through entries and transactions instead export const operationsRelations = relations(operationsTable, ({ one }) => ({ transaction: one(transactionsTable, { - fields: [operationsTable.transactionId], - references: [transactionsTable.id], + fields: [operationsTable.userId, operationsTable.transactionId], + references: [transactionsTable.userId, transactionsTable.id], }), })); diff --git a/apps/backend/src/db/schemas/transactions.ts b/apps/backend/src/db/schemas/transactions.ts index 7269b673..c12b7662 100644 --- a/apps/backend/src/db/schemas/transactions.ts +++ b/apps/backend/src/db/schemas/transactions.ts @@ -1,6 +1,12 @@ import { UUID } from '@ledgerly/shared/types'; import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm'; -import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { + foreignKey, + index, + sqliteTable, + text, + uniqueIndex, +} from 'drizzle-orm/sqlite-core'; import { commoditiesTable, OperationDbRow, operationsTable } from '../schema'; @@ -18,10 +24,7 @@ import { usersTable } from './users'; export const transactionsTable = sqliteTable( 'transactions', { - commodityId: text('commodity_id') - .notNull() - .references(() => commoditiesTable.id) - .$type(), + commodityId: text('commodity_id').notNull().$type(), createdAt, description, id, @@ -36,10 +39,20 @@ export const transactionsTable = sqliteTable( version, }, - (t) => [index('idx_transactions_user_date').on(t.userId, t.transactionDate)], + (table) => [ + foreignKey({ + columns: [table.userId, table.commodityId], + foreignColumns: [commoditiesTable.userId, commoditiesTable.id], + name: 'transactions_user_id_commodity_id_commodities_user_id_id_fk', + }), + index('idx_transactions_user_date').on(table.userId, table.transactionDate), + uniqueIndex('transactions_user_id_id_unique_idx').on( + table.userId, + table.id, + ), + ], ); -// TODO: check if relations are needed for operations, or if they can be accessed through entries and transactions instead export const transactionsRelations = relations( transactionsTable, ({ many }) => ({ diff --git a/apps/backend/src/db/test-db.ts b/apps/backend/src/db/test-db.ts index 10a126d3..ea35d300 100644 --- a/apps/backend/src/db/test-db.ts +++ b/apps/backend/src/db/test-db.ts @@ -14,7 +14,7 @@ import { } from '@ledgerly/shared/types'; import { isoDate, isoDatetime } from '@ledgerly/shared/validation'; import { createClient } from '@libsql/client'; -import { eq, sql } from 'drizzle-orm'; +import { and, eq, sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; import { DataBase } from 'src/db'; @@ -356,7 +356,7 @@ export class TestDB { const transaction = await this.db .select() .from(schema.transactionsTable) - .where(sql`${schema.transactionsTable.id} = ${transactionId}`) + .where(eq(schema.transactionsTable.id, transactionId)) .get(); return transaction ?? null; @@ -372,7 +372,7 @@ export class TestDB { const operations = await this.db .select() .from(schema.operationsTable) - .where(sql`${schema.operationsTable.transactionId} = ${transactionId}`); + .where(eq(schema.operationsTable.transactionId, transactionId)); return { operations, ...transaction }; }; @@ -381,7 +381,7 @@ export class TestDB { return await this.db .update(schema.transactionsTable) .set({ isTombstone: true }) - .where(sql`${schema.transactionsTable.id} = ${transactionId}`) + .where(eq(schema.transactionsTable.id, transactionId)) .returning() .get(); }; @@ -479,8 +479,7 @@ export class TestDB { const account = await this.db .select() .from(accountsTable) - .where(sql`${accountsTable.id} = ${accountId} `) - + .where(eq(accountsTable.id, accountId)) .get(); return account ?? null; @@ -542,7 +541,10 @@ export class TestDB { .select() .from(schema.operationsTable) .where( - sql`${schema.operationsTable.accountId} = ${accountId} AND ${schema.operationsTable.userId} = ${userId}`, + and( + eq(schema.operationsTable.accountId, accountId), + eq(schema.operationsTable.userId, userId), + ), ); return operations; @@ -552,7 +554,7 @@ export class TestDB { const operation = await this.db .select() .from(schema.operationsTable) - .where(sql`${schema.operationsTable.id} = ${operationId}`) + .where(eq(schema.operationsTable.id, operationId)) .get(); return operation ?? null; @@ -563,7 +565,10 @@ export class TestDB { .select() .from(schema.operationsTable) .where( - sql`${schema.operationsTable.transactionId} = ${transactionId} AND ${schema.operationsTable.userId} = ${userId}`, + and( + eq(schema.operationsTable.transactionId, transactionId), + eq(schema.operationsTable.userId, userId), + ), ); return operations; @@ -573,7 +578,7 @@ export class TestDB { const commodity = await this.db .select() .from(schema.commoditiesTable) - .where(sql`${schema.commoditiesTable.id} = ${commodityId}`) + .where(eq(schema.commoditiesTable.id, commodityId)) .get(); return commodity ?? null; diff --git a/apps/backend/src/infrastructure/db/BaseRepository.ts b/apps/backend/src/infrastructure/db/BaseRepository.ts index 3cad24bf..cf392e33 100644 --- a/apps/backend/src/infrastructure/db/BaseRepository.ts +++ b/apps/backend/src/infrastructure/db/BaseRepository.ts @@ -1,11 +1,11 @@ import { type ErrorContextByCode, UUID } from '@ledgerly/shared/types'; import { DBErrorContext, + DatabaseError, DatabaseOperationError, ForbiddenAccessError, ForeignKeyConstraintError, InfrastructureError, - InvalidDataError, RecordAlreadyExistsError, RepositoryNotFoundError, } from 'src/infrastructure/errors'; @@ -78,7 +78,7 @@ export class BaseRepository { } } - if (error instanceof InvalidDataError) throw error; + if (error instanceof DatabaseError) throw error; if (error instanceof InfrastructureError) throw error; const databaseError = new DatabaseOperationError({ diff --git a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts index b98a3281..6cea15bc 100644 --- a/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts +++ b/apps/backend/src/infrastructure/db/operations/operation.repository.test.ts @@ -8,7 +8,10 @@ import { import { Transaction } from 'src/domain'; import { Amount, Timestamp } from 'src/domain/domain-core'; import { OperationSnapshot } from 'src/domain/operations/types'; -import { RepositoryInvariantError } from 'src/infrastructure/errors'; +import { + ForeignKeyConstraintError, + RepositoryInvariantError, +} from 'src/infrastructure/errors'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { TestDB } from '../../../db/test-db'; @@ -182,6 +185,62 @@ describe('OperationRepository', () => { }); }); + it('should reject operation insert with ForeignKeyConstraintError when account belongs to another user at DB level', async () => { + const otherUser = await testDB.createUser({ + email: `other-${Date.now()}@example.com`, + }); + + const otherCommodity = await testDB.createCommodity(otherUser.id); + + const otherAccount = await testDB.createAccount( + otherUser.id, + otherCommodity.id, + ); + + const operation = OperationPersistenceMapper.toDBRow(data.operations[0]); + + const operationWithOtherAccount = { + ...operation, + accountId: otherAccount.id, + }; + + await expect( + operationRepository.save( + userSnapshot.id, + [operationWithOtherAccount], + new Map(), + ), + ).rejects.toThrow(ForeignKeyConstraintError); + }); + + it('should reject operation insert with ForeignKeyConstraintError when transaction belongs to another user at DB level', async () => { + const otherUser = await testDB.createUser({ + email: `other-${Date.now()}@example.com`, + }); + + const otherCommodity = await testDB.createCommodity(otherUser.id); + + const otherTransaction = await testDB.createTransaction( + otherUser.id, + otherCommodity.id, + ); + + const operation = OperationPersistenceMapper.toDBRow(data.operations[0]); + + const operationWithOtherTransaction = { + ...operation, + transactionId: otherTransaction.id, + }; + + await expect( + operationRepository.save( + userSnapshot.id, + [operationWithOtherTransaction], + new Map(), + ), + ).rejects.toThrow(ForeignKeyConstraintError); + }); + it('should update and delete operations successfully based on the snapshot', async () => { const operations = data.operations.map((operation) => OperationPersistenceMapper.toDBRow(operation), diff --git a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts index 78ab9025..f145cc65 100644 --- a/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts +++ b/apps/backend/src/infrastructure/db/transaction/transaction.repository.test.ts @@ -7,7 +7,7 @@ import { TransactionBuilder, TransactionPersistenceBuilderResult, } from 'src/db/test-utils'; -import { Account } from 'src/domain'; +import { Account, Transaction } from 'src/domain'; import { Amount, CommodityCode, @@ -367,6 +367,28 @@ describe('TransactionRepository', () => { ).rejects.toThrow(RepositoryNotFoundError); }); + it('should reject transaction create with RepositoryNotFoundError when commodity belongs to another user', async () => { + const otherUser = await testDB.createUser(); + const otherCommodity = await testDB.createCommodity(otherUser.id); + + const transaction = data.transaction; + + const transactionSnapshot = transaction.toSnapshot(); + + const transactionSnapshotWithOtherCommodity = { + ...transactionSnapshot, + commodityId: otherCommodity.id, + }; + + const transactionWithOtherCommodity = Transaction.restore( + transactionSnapshotWithOtherCommodity, + ); + + await expect( + transactionRepository.create(user.id, transactionWithOtherCommodity), + ).rejects.toThrow(RepositoryNotFoundError); + }); + it('should not create a transaction when commodity is tombstoned', async () => { const transaction = data.transaction; const commodityId = transaction.toSnapshot().commodityId; diff --git a/apps/backend/src/presentation/http/error-handler.test.ts b/apps/backend/src/presentation/http/error-handler.test.ts index 195892fb..b5745799 100644 --- a/apps/backend/src/presentation/http/error-handler.test.ts +++ b/apps/backend/src/presentation/http/error-handler.test.ts @@ -272,6 +272,47 @@ describe('errorHandler', () => { }); }); + it.each([ + [401, apiErrorCodes.unauthorized, {}], + [403, apiErrorCodes.unauthorizedAccess, { entityType: 'request' }], + [404, apiErrorCodes.entityNotFound, { entityType: 'route' }], + [409, apiErrorCodes.conflict, {}], + [415, apiErrorCodes.badRequest, {}], + ])( + 'maps Fastify status %i to the matching stable API error code', + (statusCode, expectedCode, expectedContext) => { + const error = Object.assign(new Error('fastify diagnostic'), { + code: 'FST_ERR_TEST', + statusCode, + }); + + expect(handle(error)).toEqual({ + payload: { + code: expectedCode, + context: expectedContext, + error: true, + }, + statusCode, + }); + }, + ); + + it('does not classify Fastify server errors as bad request', () => { + const error = Object.assign(new Error('server diagnostic'), { + code: 'FST_ERR_INTERNAL_SERVER', + statusCode: 500, + }); + + expect(handle(error)).toEqual({ + payload: { + code: apiErrorCodes.internalServerError, + context: {}, + error: true, + }, + statusCode: 500, + }); + }); + it('uses a safe, stable response for unknown errors', () => { const response = handle(new Error('unhandled secret diagnostic')); diff --git a/apps/backend/src/presentation/http/error-handler.ts b/apps/backend/src/presentation/http/error-handler.ts index 50c5e2ee..c1df187c 100644 --- a/apps/backend/src/presentation/http/error-handler.ts +++ b/apps/backend/src/presentation/http/error-handler.ts @@ -108,6 +108,68 @@ const isFastifyError = (error: unknown): error is FastifyError => { ); }; +type FastifyErrorWithStatus = FastifyError & { + status?: number; +}; + +const getFastifyErrorStatus = (error: FastifyError): number => { + const status = error.statusCode ?? (error as FastifyErrorWithStatus).status; + + return typeof status === 'number' && status >= 400 && status < 600 + ? status + : statusByErrorCode[apiErrorCodes.internalServerError]; +}; + +type CodedErrorPayload = { + [Code in ApiErrorCode]: { + code: Code; + context: ErrorContextByCode[Code]; + error: true; + }; +}[ApiErrorCode]; + +const fastifyErrorPayloadByStatus: Partial> = + { + 401: { + code: apiErrorCodes.unauthorized, + context: {}, + error: true, + }, + 403: { + code: apiErrorCodes.unauthorizedAccess, + context: { entityType: 'request' }, + error: true, + }, + 404: { + code: apiErrorCodes.entityNotFound, + context: { entityType: 'route' }, + error: true, + }, + 409: { + code: apiErrorCodes.conflict, + context: {}, + error: true, + }, + } satisfies Partial>; + +const getFastifyErrorPayload = (status: number): CodedErrorPayload => { + if (status >= 500) { + return { + code: apiErrorCodes.internalServerError, + context: {}, + error: true, + }; + } + + return ( + fastifyErrorPayloadByStatus[status] ?? { + code: apiErrorCodes.badRequest, + context: {}, + error: true, + } + ); +}; + export function errorHandler( error: FastifyError | Error, _request: FastifyRequest, @@ -123,12 +185,9 @@ export function errorHandler( } if (isFastifyError(error)) { - return sendCodedError( - reply, - statusByErrorCode[apiErrorCodes.badRequest], - apiErrorCodes.badRequest, - {}, - ); + const status = getFastifyErrorStatus(error); + + return reply.status(status).send(getFastifyErrorPayload(status)); } if (error instanceof ZodError) { diff --git a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts index 581e850b..146fea9f 100644 --- a/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts +++ b/apps/backend/src/presentation/http/transactions/transaction.integration.test.ts @@ -613,10 +613,11 @@ describe('Transactions Integration Tests', () => { it("should return 404 when accessing another user's transaction", async () => { const otherUser = await testDB.createUser(); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); const otherUserTransaction = await testDB.createTransaction( otherUser.id, - commodityId, + otherUserCommodity.id, ); const response = await server.inject({ @@ -1149,10 +1150,11 @@ describe('Transactions Integration Tests', () => { email: 'otheruser@example.com', password: 'password123', }); + const otherUserCommodity = await testDB.createCommodity(otherUser.id); const otherUserTransaction = await testDB.createTransaction( otherUser.id, - commodityId, + otherUserCommodity.id, ); const response = await server.inject({ @@ -1763,9 +1765,10 @@ describe('Transactions Integration Tests', () => { it("should return 404 when updating another user's transaction", async () => { const another = await testDB.createUser(); + const anotherCommodity = await testDB.createCommodity(another.id); const transaction = await testDB.createTransaction( another.id, - commodityId, + anotherCommodity.id, ); const updatedData = createUpdateRequest(); diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index 4e373d9b..cb5e6234 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -360,9 +360,9 @@ USERS (root entity) ### Data Integrity 1. **User ownership**: user-owned records must not reference entities owned by - another user. Account-to-Commodity ownership is enforced with a composite - foreign key. Additional same-user composite foreign keys for transaction and - operation relations are tracked by LED-132. + another user. Account-to-Commodity, Transaction-to-Commodity, + Operation-to-Account, and Operation-to-Transaction ownership are enforced with + same-user composite foreign keys. 2. **Required relations**: each operation must have a transaction and account. 3. **Soft deletes**: entities use `isTombstone` where the deletion must be retained in storage. diff --git a/docs/architecture/adr/0021-split-commodity-reference-business-policy-from-persistence-integrity.md b/docs/architecture/adr/0021-split-commodity-reference-business-policy-from-persistence-integrity.md index 0496f688..8b90d74d 100644 --- a/docs/architecture/adr/0021-split-commodity-reference-business-policy-from-persistence-integrity.md +++ b/docs/architecture/adr/0021-split-commodity-reference-business-policy-from-persistence-integrity.md @@ -79,8 +79,8 @@ inputs because they have user-facing error semantics. - Use case and integration tests should cover user-visible business error contracts. - ADR 0019 is superseded by this decision. -- Additional same-user composite foreign keys for transactions and operations - are tracked separately by LED-132. +- Same-user composite foreign keys for transaction and operation relations are + implemented by LED-132. ## Related diff --git a/packages/shared/src/validation/accounts.test.ts b/packages/shared/src/validation/accounts.test.ts index de754b7b..2f97f307 100644 --- a/packages/shared/src/validation/accounts.test.ts +++ b/packages/shared/src/validation/accounts.test.ts @@ -62,4 +62,10 @@ describe("accountQuerySchema", () => { status: "all", }); }); + + it("allows querying closed accounts", () => { + expect(accountQuerySchema.parse({ status: "closed" })).toEqual({ + status: "closed", + }); + }); });