diff --git a/apps/server/.gitignore b/apps/server/.gitignore new file mode 100644 index 0000000..16c66db --- /dev/null +++ b/apps/server/.gitignore @@ -0,0 +1,2 @@ +# Uploaded files +uploads diff --git a/apps/server/migrations/20260120142449_vehicle-soft-delete/migration.sql b/apps/server/migrations/20260120142449_vehicle-soft-delete/migration.sql new file mode 100644 index 0000000..387e2a3 --- /dev/null +++ b/apps/server/migrations/20260120142449_vehicle-soft-delete/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `vehicle` ADD `deleted` integer DEFAULT false; \ No newline at end of file diff --git a/apps/server/migrations/20260120142449_vehicle-soft-delete/snapshot.json b/apps/server/migrations/20260120142449_vehicle-soft-delete/snapshot.json new file mode 100644 index 0000000..91914db --- /dev/null +++ b/apps/server/migrations/20260120142449_vehicle-soft-delete/snapshot.json @@ -0,0 +1,242 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "0acb2ffa-98cb-489f-b34d-8cd642af5a5f", + "prevIds": [ + "fe2a294b-7a2a-4824-959e-50683f28c48c" + ], + "ddl": [ + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260120143652_operation-soft-delete/migration.sql b/apps/server/migrations/20260120143652_operation-soft-delete/migration.sql new file mode 100644 index 0000000..5bc114a --- /dev/null +++ b/apps/server/migrations/20260120143652_operation-soft-delete/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `operations` ADD `deleted` integer DEFAULT false; \ No newline at end of file diff --git a/apps/server/migrations/20260120143652_operation-soft-delete/snapshot.json b/apps/server/migrations/20260120143652_operation-soft-delete/snapshot.json new file mode 100644 index 0000000..e9d46fa --- /dev/null +++ b/apps/server/migrations/20260120143652_operation-soft-delete/snapshot.json @@ -0,0 +1,252 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "afe42157-5b57-4c80-8334-87cefa779847", + "prevIds": [ + "0acb2ffa-98cb-489f-b34d-8cd642af5a5f" + ], + "ddl": [ + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260120180340_documents/migration.sql b/apps/server/migrations/20260120180340_documents/migration.sql new file mode 100644 index 0000000..39a34d1 --- /dev/null +++ b/apps/server/migrations/20260120180340_documents/migration.sql @@ -0,0 +1,13 @@ +CREATE TABLE `documents` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `date` integer, + `mileage` integer, + `uri` text, + `note` text, + `type` text NOT NULL, + `documentable_id` integer NOT NULL, + `entityType` text, + `deleted` integer DEFAULT false, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL +); diff --git a/apps/server/migrations/20260120180340_documents/snapshot.json b/apps/server/migrations/20260120180340_documents/snapshot.json new file mode 100644 index 0000000..2290825 --- /dev/null +++ b/apps/server/migrations/20260120180340_documents/snapshot.json @@ -0,0 +1,375 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "48fca5de-a556-4773-b32b-f66883ec420b", + "prevIds": [ + "afe42157-5b57-4c80-8334-87cefa779847" + ], + "ddl": [ + { + "name": "documents", + "entityType": "tables" + }, + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uri", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "documentable_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "entityType", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_pk", + "table": "documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260122094424_documents_types/migration.sql b/apps/server/migrations/20260122094424_documents_types/migration.sql new file mode 100644 index 0000000..59aa9ad --- /dev/null +++ b/apps/server/migrations/20260122094424_documents_types/migration.sql @@ -0,0 +1,8 @@ +CREATE TABLE `documents_types` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `name` text, + `slug` text +); +--> statement-breakpoint +ALTER TABLE `documents` ADD `type_id` integer REFERENCES documents_types(id);--> statement-breakpoint +ALTER TABLE `documents` DROP COLUMN `type`; \ No newline at end of file diff --git a/apps/server/migrations/20260122094424_documents_types/snapshot.json b/apps/server/migrations/20260122094424_documents_types/snapshot.json new file mode 100644 index 0000000..1b55e37 --- /dev/null +++ b/apps/server/migrations/20260122094424_documents_types/snapshot.json @@ -0,0 +1,433 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "384f811a-3a8b-41a7-8cf2-3c563226f2b6", + "prevIds": [ + "48fca5de-a556-4773-b32b-f66883ec420b" + ], + "ddl": [ + { + "name": "documents", + "entityType": "tables" + }, + { + "name": "documents_types", + "entityType": "tables" + }, + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uri", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "documentable_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "entityType", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "type_id" + ], + "tableTo": "documents_types", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_documents_type_id_documents_types_id_fk", + "entityType": "fks", + "table": "documents" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_pk", + "table": "documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_types_pk", + "table": "documents_types", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260122102851_documents_types_seed/migration.sql b/apps/server/migrations/20260122102851_documents_types_seed/migration.sql new file mode 100644 index 0000000..91bd9ab --- /dev/null +++ b/apps/server/migrations/20260122102851_documents_types_seed/migration.sql @@ -0,0 +1 @@ +INSERT INTO "documents_types" ("id", "slug", "name") VALUES(1, 'cover', 'Cover image'); diff --git a/apps/server/migrations/20260122102851_documents_types_seed/snapshot.json b/apps/server/migrations/20260122102851_documents_types_seed/snapshot.json new file mode 100644 index 0000000..eb6178a --- /dev/null +++ b/apps/server/migrations/20260122102851_documents_types_seed/snapshot.json @@ -0,0 +1,433 @@ +{ + "id": "1c6671d2-60c3-44c2-ae22-6a30921e33cb", + "prevIds": [ + "384f811a-3a8b-41a7-8cf2-3c563226f2b6" + ], + "version": "7", + "dialect": "sqlite", + "ddl": [ + { + "name": "documents", + "entityType": "tables" + }, + { + "name": "documents_types", + "entityType": "tables" + }, + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uri", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "documentable_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "entityType", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "type_id" + ], + "tableTo": "documents_types", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_documents_type_id_documents_types_id_fk", + "entityType": "fks", + "table": "documents" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_pk", + "table": "documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_types_pk", + "table": "documents_types", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260129172850_elite_stephen_strange/migration.sql b/apps/server/migrations/20260129172850_elite_stephen_strange/migration.sql new file mode 100644 index 0000000..c76c3c5 --- /dev/null +++ b/apps/server/migrations/20260129172850_elite_stephen_strange/migration.sql @@ -0,0 +1,12 @@ +ALTER TABLE `documents` ADD `name` text NOT NULL DEFAULT 'Unnamed Document';--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_documents_types` ( + `id` integer PRIMARY KEY AUTOINCREMENT, + `name` text NOT NULL, + `slug` text NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_documents_types`(`id`, `name`, `slug`) SELECT `id`, `name`, `slug` FROM `documents_types`;--> statement-breakpoint +DROP TABLE `documents_types`;--> statement-breakpoint +ALTER TABLE `__new_documents_types` RENAME TO `documents_types`;--> statement-breakpoint +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/apps/server/migrations/20260129172850_elite_stephen_strange/snapshot.json b/apps/server/migrations/20260129172850_elite_stephen_strange/snapshot.json new file mode 100644 index 0000000..7b54e3d --- /dev/null +++ b/apps/server/migrations/20260129172850_elite_stephen_strange/snapshot.json @@ -0,0 +1,443 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "484a4e9f-0ad0-4b00-b776-1fa2d825125e", + "prevIds": [ + "1c6671d2-60c3-44c2-ae22-6a30921e33cb" + ], + "ddl": [ + { + "name": "documents", + "entityType": "tables" + }, + { + "name": "documents_types", + "entityType": "tables" + }, + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uri", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "documentable_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "entityType", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "type_id" + ], + "tableTo": "documents_types", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_documents_type_id_documents_types_id_fk", + "entityType": "fks", + "table": "documents" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_pk", + "table": "documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_types_pk", + "table": "documents_types", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/migrations/20260210190831_operations_add_name/migration.sql b/apps/server/migrations/20260210190831_operations_add_name/migration.sql new file mode 100644 index 0000000..bf14549 --- /dev/null +++ b/apps/server/migrations/20260210190831_operations_add_name/migration.sql @@ -0,0 +1 @@ +ALTER TABLE `operations` ADD `name` text NOT NULL; \ No newline at end of file diff --git a/apps/server/migrations/20260210190831_operations_add_name/snapshot.json b/apps/server/migrations/20260210190831_operations_add_name/snapshot.json new file mode 100644 index 0000000..f945b22 --- /dev/null +++ b/apps/server/migrations/20260210190831_operations_add_name/snapshot.json @@ -0,0 +1,453 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "d310475a-6017-4698-be8b-e018884cc79f", + "prevIds": [ + "484a4e9f-0ad0-4b00-b776-1fa2d825125e" + ], + "ddl": [ + { + "name": "documents", + "entityType": "tables" + }, + { + "name": "documents_types", + "entityType": "tables" + }, + { + "name": "operations", + "entityType": "tables" + }, + { + "name": "vehicle", + "entityType": "tables" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uri", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "documentable_id", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "entityType", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "documents" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "documents_types" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "date", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mileage", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "note", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vehicle_id", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "operations" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "brand", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "deleted", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "engine", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "power", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trim", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "year", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "vehicle" + }, + { + "columns": [ + "type_id" + ], + "tableTo": "documents_types", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_documents_type_id_documents_types_id_fk", + "entityType": "fks", + "table": "documents" + }, + { + "columns": [ + "vehicle_id" + ], + "tableTo": "vehicle", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_operations_vehicle_id_vehicle_id_fk", + "entityType": "fks", + "table": "operations" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_pk", + "table": "documents", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "documents_types_pk", + "table": "documents_types", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "operations_pk", + "table": "operations", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "vehicle_pk", + "table": "vehicle", + "entityType": "pks" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/apps/server/openapi.json b/apps/server/openapi.json new file mode 100644 index 0000000..8c87ca5 --- /dev/null +++ b/apps/server/openapi.json @@ -0,0 +1,3893 @@ +{ + "info": { + "title": "CM3K API", + "version": "1.0.0" + }, + "openapi": "3.1.1", + "paths": { + "/operations": { + "get": { + "operationId": "operations.list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + } + } + } + }, + "/vehicles": { + "post": { + "operationId": "vehicles.vehicles.create", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "brand": { + "type": "string" + }, + "description": { + "type": "string" + }, + "engine": { + "type": "string" + }, + "model": { + "type": "string" + }, + "power": { + "type": "number" + }, + "trim": { + "type": "string" + }, + "year": { + "type": "number" + } + }, + "required": [ + "brand", + "description", + "engine", + "model", + "power", + "trim", + "year" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "brand": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "engine": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "power": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "trim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "year": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "brand", + "description", + "engine", + "model", + "power", + "trim", + "year" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + }, + "get": { + "operationId": "vehicles.vehicles.list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "brand": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "engine": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "power": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "trim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "year": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "brand", + "description", + "engine", + "model", + "power", + "trim", + "year" + ] + } + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/vehicles/{id}": { + "get": { + "operationId": "vehicles.vehicles.get", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "brand": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "engine": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "power": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "trim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "year": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "brand", + "description", + "engine", + "model", + "power", + "trim", + "year" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + }, + "delete": { + "operationId": "vehicles.vehicles.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "required": [] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + }, + "patch": { + "operationId": "vehicles.vehicles.update", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "brand": { + "type": "string" + }, + "description": { + "type": "string" + }, + "engine": { + "type": "string" + }, + "model": { + "type": "string" + }, + "power": { + "type": "number" + }, + "trim": { + "type": "string" + }, + "year": { + "type": "number" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "brand": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "engine": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "model": { + "type": "string" + }, + "power": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "trim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "year": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "brand", + "description", + "engine", + "model", + "power", + "trim", + "year" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/vehicles/{vehicleId}/operations": { + "post": { + "operationId": "vehicles.vehicles.operations.create", + "parameters": [ + { + "name": "vehicleId", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "mileage": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "note": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "date", + "mileage", + "note", + "type" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "mileage": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "date", + "mileage", + "note", + "type" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + }, + "get": { + "operationId": "vehicles.vehicles.operations.list", + "parameters": [ + { + "name": "vehicleId", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "mileage": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "date", + "mileage", + "note", + "type" + ] + } + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/vehicles/{vehicleId}/{id}": { + "delete": { + "operationId": "vehicles.vehicles.operations.remove", + "parameters": [ + { + "name": "vehicleId", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + }, + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "const": true + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/vehicles/{vehicleId}/documents": { + "post": { + "operationId": "vehicles.vehicles.documents.create", + "parameters": [ + { + "name": "vehicleId", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "date": { + "type": "string", + "format": "date-time" + }, + "mileage": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "file": { + "not": {} + }, + "note": { + "type": "string" + }, + "typeId": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "name", + "file", + "typeId" + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "mileage": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "uri": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "name", + "date", + "mileage", + "uri", + "note", + "type" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + }, + "get": { + "operationId": "vehicles.vehicles.documents.list", + "parameters": [ + { + "name": "vehicleId", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + }, + "date": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "mileage": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "uri": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "note": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name" + ] + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "name", + "date", + "mileage", + "uri", + "note", + "type" + ] + } + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/documents/{id}": { + "delete": { + "operationId": "documents.documents.remove", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {}, + "required": [] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "not": {} + } + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + }, + "/document-types": { + "get": { + "operationId": "documentType.documentTypes.list", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "NOT_FOUND" + }, + "status": { + "const": 404 + }, + "message": { + "type": "string", + "default": "Not Found" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "418": { + "description": "418", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "SKILL_ISSUE" + }, + "status": { + "const": 418 + }, + "message": { + "type": "string", + "default": "SKILL_ISSUE" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "defined": { + "const": true + }, + "code": { + "const": "INPUT_VALIDATION_FAILED" + }, + "status": { + "const": 422 + }, + "message": { + "type": "string", + "default": "INPUT_VALIDATION_FAILED" + }, + "data": { + "type": "object", + "properties": { + "fieldErrors": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ] + } + }, + "formErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fieldErrors", + "formErrors" + ] + } + }, + "required": [ + "defined", + "code", + "status", + "message", + "data" + ] + }, + { + "type": "object", + "properties": { + "defined": { + "const": false + }, + "code": { + "type": "string" + }, + "status": { + "type": "number" + }, + "message": { + "type": "string" + }, + "data": {} + }, + "required": [ + "defined", + "code", + "status", + "message" + ] + } + ] + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 6b96bba..7b9bae5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -9,7 +9,8 @@ }, "scripts": { "dev": "bun run --hot src/index.ts", - "openapi": "bun run --hot src/openapi.ts", + "openapi": "bun run --hot src/openapi/openapi.ts", + "openapi:generate": "bun run src/openapi/openapi-generate.ts", "lint": "oxlint", "format": "oxfmt", "test": "vitest", @@ -23,7 +24,7 @@ "@cm3k/contract": "workspace:*", "@cm3k/validation": "workspace:*", "@hono/swagger-ui": "^0.5.3", - "@libsql/client": "^0.15.15", + "@libsql/client": "^0.17.0", "@orpc/client": "^1.13.4", "@orpc/experimental-pino": "^1.13.4", "@orpc/openapi": "^1.13.4", @@ -32,25 +33,25 @@ "drizzle-orm": "^1.0.0-beta.9-e89174b", "drizzle-seed": "^1.0.0-beta.9-e89174b", "drizzle-zod": "^0.8.3", - "hono": "^4.11.4", + "hono": "^4.11.7", "hono-pino": "^0.10.3", - "pino": "^10.2.1", + "pino": "^10.3.0", "pino-pretty": "^13.1.3", "swagger-ui": "^5.31.0", "swagger-ui-dist": "^5.31.0", "true-myth": "^9.3.1", - "zod": "^4.3.5" + "zod": "^4.3.6" }, "devDependencies": { - "@types/bun": "^1.3.6", + "@types/bun": "^1.3.8", "@types/swagger-ui": "^5.21.1", "@types/swagger-ui-dist": "^3.30.6", - "@vitest/coverage-v8": "4.0.15", - "@vitest/ui": "^4.0.17", + "@vitest/coverage-v8": "4.0.18", + "@vitest/ui": "^4.0.18", "drizzle-kit": "^1.0.0-beta.9-e89174b", - "globals": "^16.5.0", - "oxfmt": "^0.21.0", - "oxlint": "^1.41.0", - "vitest": "^4.0.17" + "globals": "^17.3.0", + "oxfmt": "^0.28.0", + "oxlint": "^1.43.0", + "vitest": "^4.0.18" } } diff --git a/apps/server/src/db/schemas/documentTypes.ts b/apps/server/src/db/schemas/documentTypes.ts new file mode 100644 index 0000000..7f3989e --- /dev/null +++ b/apps/server/src/db/schemas/documentTypes.ts @@ -0,0 +1,7 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const documentsTypes = sqliteTable("documents_types", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + slug: text().notNull(), +}); diff --git a/apps/server/src/db/schemas/documents.ts b/apps/server/src/db/schemas/documents.ts new file mode 100644 index 0000000..6436229 --- /dev/null +++ b/apps/server/src/db/schemas/documents.ts @@ -0,0 +1,18 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +import { timestamps } from "../helpers/timestamps"; +import { documentsTypes } from "./documentTypes"; + +export const documents = sqliteTable("documents", { + id: integer().primaryKey({ autoIncrement: true }), + name: text().notNull(), + date: integer({ mode: "timestamp" }), + mileage: integer(), + uri: text(), + note: text(), + typeId: integer("type_id").references(() => documentsTypes.id), + entityId: integer("documentable_id").notNull(), + entityType: text({ enum: ["vehicle", "operation"] }), + deleted: integer({ mode: "boolean" }).default(false), + ...timestamps, +}); diff --git a/apps/server/src/db/schemas/operations.ts b/apps/server/src/db/schemas/operations.ts index c40745a..3d879cb 100644 --- a/apps/server/src/db/schemas/operations.ts +++ b/apps/server/src/db/schemas/operations.ts @@ -6,11 +6,13 @@ import { vehicles } from "./vehicle"; export const operations = sqliteTable("operations", { id: integer().primaryKey({ autoIncrement: true }), date: integer({ mode: "timestamp" }), + name: text().notNull(), mileage: integer(), note: text(), type: text().notNull(), vehicleId: integer("vehicle_id") .notNull() .references(() => vehicles.id), + deleted: integer({ mode: "boolean" }).default(false), ...timestamps, }); diff --git a/apps/server/src/db/schemas/relations.ts b/apps/server/src/db/schemas/relations.ts index 636643a..5d56255 100644 --- a/apps/server/src/db/schemas/relations.ts +++ b/apps/server/src/db/schemas/relations.ts @@ -1,19 +1,37 @@ import { defineRelations } from "drizzle-orm"; +import { documents } from "./documents"; +import { documentsTypes } from "./documentTypes"; import { operations } from "./operations"; import { vehicles } from "./vehicle"; -export const relations = defineRelations({ operations, vehicles }, (r) => ({ - operations: { - vehicles: r.one.vehicles({ - from: r.operations.vehicleId, - to: r.vehicles.id, - }), - }, - vehicles: { - operations: r.many.operations({ - from: r.vehicles.id, - to: r.operations.vehicleId, - }), - }, -})); +export const relations = defineRelations( + { operations, vehicles, documents, documentsTypes }, + (r) => ({ + operations: { + vehicles: r.one.vehicles({ + from: r.operations.vehicleId, + to: r.vehicles.id, + }), + }, + documents: { + type: r.one.documentsTypes({ + from: r.documents.typeId, + to: r.documentsTypes.id, + }), + }, + vehicles: { + operations: r.many.operations({ + from: r.vehicles.id, + to: r.operations.vehicleId, + }), + documents: r.many.documents({ + from: r.vehicles.id, + to: r.documents.entityId, + where: { + entityType: "vehicle", + }, + }), + }, + }), +); diff --git a/apps/server/src/db/schemas/vehicle.ts b/apps/server/src/db/schemas/vehicle.ts index 19cd775..e4c6f8e 100644 --- a/apps/server/src/db/schemas/vehicle.ts +++ b/apps/server/src/db/schemas/vehicle.ts @@ -6,6 +6,7 @@ export const vehicles = sqliteTable("vehicle", { id: integer().primaryKey({ autoIncrement: true }), brand: text().notNull(), description: text(), + deleted: integer({ mode: "boolean" }).default(false), engine: text(), model: text().notNull(), power: integer(), diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2142fbd..3afa4ea 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -5,6 +5,7 @@ import { type RouterClient } from "@orpc/server"; import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4"; import { Hono } from "hono"; import { pinoLogger } from "hono-pino"; +import { serveStatic } from "hono/bun"; import { cors } from "hono/cors"; import { requestId } from "hono/request-id"; @@ -38,6 +39,8 @@ app.use( }), ); +app.use("/uploads/*", serveStatic({ root: "./" })); + app.use("/*", async (c, next) => { const apiResult = await apiHandler.handle(c.req.raw, { prefix: "/api", diff --git a/apps/server/src/lib/serviceErrors.ts b/apps/server/src/lib/serviceErrors.ts index dd2f1a1..c4d5a87 100644 --- a/apps/server/src/lib/serviceErrors.ts +++ b/apps/server/src/lib/serviceErrors.ts @@ -1,4 +1,4 @@ -type ServiceErrorName = "NotFoundError" | "DbError"; +type ServiceErrorName = "NotFoundError" | "DbError" | "FileError"; export class ServiceError extends Error { readonly name: ServiceErrorName; @@ -25,3 +25,14 @@ export class DbError extends ServiceError { } } } + +export class FileError extends ServiceError { + readonly name = "FileError"; + readonly originalError?: Error; + constructor(originalError?: unknown, message?: string) { + super("FileError", message ?? "File error"); + if (originalError instanceof Error) { + this.originalError = originalError; + } + } +} diff --git a/apps/server/src/modules/document-types/router.ts b/apps/server/src/modules/document-types/router.ts new file mode 100644 index 0000000..f60d4fa --- /dev/null +++ b/apps/server/src/modules/document-types/router.ts @@ -0,0 +1,29 @@ +import { documentTypesContract } from "@cm3k/contract"; +import { implement } from "@orpc/server"; + +import { createDocumentType, listDocumentTypes } from "./service"; + +const o = implement(documentTypesContract); + +const list = o.documentTypes.list.handler(async () => { + const result = await listDocumentTypes(); + if (result.isErr) { + throw result.error; + } + return result.value; +}); + +const create = o.documentTypes.create.handler(async ({ input }) => { + const documentType = await createDocumentType(input); + if (documentType.isErr) { + throw documentType.error; + } + return documentType.value; +}); + +export const documentTypesRouter = o.router({ + documentTypes: { + list, + create, + }, +}); diff --git a/apps/server/src/modules/document-types/service.ts b/apps/server/src/modules/document-types/service.ts new file mode 100644 index 0000000..e03996e --- /dev/null +++ b/apps/server/src/modules/document-types/service.ts @@ -0,0 +1,34 @@ +import { db } from "#db/index"; +import { documentsTypes } from "#db/schemas/documentTypes"; +import { DbError } from "#lib/serviceErrors"; +import { createDocumentTypeSchema } from "@cm3k/validation"; +import { ok, err } from "true-myth/result"; +import * as z from "zod/v4"; + +export const listDocumentTypes = async () => { + try { + const documentTypesList = await db.query.documentsTypes.findMany({ + columns: { + id: true, + name: true, + }, + }); + return ok(documentTypesList); + } catch (error) { + return err(new DbError(error)); + } +}; + +export const createDocumentType = async (input: z.infer) => { + try { + const { name, slug } = input; + + const [documentType] = await db + .insert(documentsTypes) + .values({ name, slug }) + .returning({ id: documentsTypes.id, name: documentsTypes.name, slug: documentsTypes.slug }); + return ok(documentType); + } catch (error) { + return err(new DbError(error)); + } +}; diff --git a/apps/server/src/modules/documents/router.test.ts b/apps/server/src/modules/documents/router.test.ts new file mode 100644 index 0000000..f9c7ecb --- /dev/null +++ b/apps/server/src/modules/documents/router.test.ts @@ -0,0 +1,19 @@ +import * as documentService from "./service"; +import { call } from "@orpc/server"; +import { ok } from "true-myth/result"; +import { beforeAll, describe, expect, it, vi, type Mocked } from "vitest"; + +import { documentsRouter } from "./router"; + +describe("/documents", () => { + describe("DELETE /documents/{id}", () => { + let spy: Mocked; + beforeAll(() => { + spy = vi.spyOn(documentService, "removeDocument").mockResolvedValue(ok()); + }); + it("calls createVehicle with correct argument", async () => { + await call(documentsRouter.documents.remove, { id: 1 }); + expect(spy).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/apps/server/src/modules/documents/router.ts b/apps/server/src/modules/documents/router.ts new file mode 100644 index 0000000..89c6159 --- /dev/null +++ b/apps/server/src/modules/documents/router.ts @@ -0,0 +1,19 @@ +import { removeDocument } from "./service"; +import { documentsContract } from "@cm3k/contract"; +import { implement } from "@orpc/server"; + +const o = implement(documentsContract); + +const remove = o.documents.remove.handler(async ({ input }) => { + console.log(input); + const result = await removeDocument(input.id); + if (result.isErr) { + throw result.error; + } +}); + +export const documentsRouter = o.router({ + documents: { + remove, + }, +}); diff --git a/apps/server/src/modules/documents/service.integration.test.ts b/apps/server/src/modules/documents/service.integration.test.ts new file mode 100644 index 0000000..601c9b4 --- /dev/null +++ b/apps/server/src/modules/documents/service.integration.test.ts @@ -0,0 +1,230 @@ +import { documents } from "#db/schemas/documents"; +import { operations } from "#db/schemas/operations"; +import { relations } from "#db/schemas/relations"; +import { vehicles } from "#db/schemas/vehicle"; +import * as fileUtils from "#utils/files"; +import { rootDir } from "#utils/paths"; +import { drizzle } from "drizzle-orm/libsql"; +import { migrate } from "drizzle-orm/libsql/migrator"; +import { reset, seed } from "drizzle-seed"; +import { ok } from "true-myth/result"; +import { beforeAll, beforeEach, describe, expect, it, Mocked, vi } from "vitest"; + +import { + createDocument, + getDocument, + listDocuments, + removeDocument, + updateDocument, +} from "./service"; + +vi.mock("#db/index", () => { + const rawDb = drizzle({ + connection: { url: ":memory:" }, + relations: relations, + }); + const migrateDb = async () => { + await migrate(rawDb, { migrationsFolder: `${rootDir}/migrations` }); + }; + + return { db: rawDb, migrateDb }; +}); + +describe("Documents service test", () => { + let dbModule: Awaited; + let spy: Mocked; + beforeAll(async () => { + dbModule = await import("#db/index"); + await dbModule.migrateDb(); + spy = vi + .spyOn(fileUtils, "saveFile") + .mockResolvedValue(ok("/uploads/test.pdf") as Awaited>); + }); + + beforeEach(async () => { + await reset(dbModule.db, { vehicles, operations }); + await dbModule.db.delete(documents); + }); + + describe("listDocuments", () => { + it("returns empty array if database is empty", async () => { + const result = await listDocuments(); + expect(result.isOk).toBe(true); + if (result.isOk) { + expect(result.value).toEqual([]); + } + }); + + it("returns all documents when no entityId is provided", async () => { + await seed(dbModule.db, { vehicles }, { count: 2 }); + + // Manually create documents + for (let i = 0; i < 6; i++) { + await dbModule.db.insert(documents).values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + deleted: false, + }); + } + + const result = await listDocuments(); + expect(result.isOk).toBe(true); + if (result.isOk) { + expect(result.value).toHaveLength(6); + } + }); + + it("returns documents for a specific vehicle", async () => { + await seed(dbModule.db, { vehicles }, { count: 2 }); + + // Manually create documents for vehicle 1 + for (let i = 0; i < 6; i++) { + await dbModule.db.insert(documents).values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + deleted: false, + }); + } + + const result = await listDocuments(1, "vehicle"); + expect(result.isOk).toBe(true); + if (result.isOk) { + expect(result.value).toHaveLength(6); + expect(result.value.every((doc) => doc.id !== undefined)).toBe(true); + } + }); + }); + + describe("getDocument", () => { + it("returns a document by id", async () => { + await seed(dbModule.db, { vehicles }, { count: 1 }); + + // Manually create a document and capture its ID + const [{ id: documentId }] = await dbModule.db + .insert(documents) + .values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + }) + .returning({ id: documents.id }); + + const result = await getDocument(documentId); + expect(result.isOk).toBe(true); + if (result.isOk) { + expect(result.value).toBeDefined(); + expect(result.value.id).toBe(documentId); + } + }); + + it("returns error if document not found", async () => { + const result = await getDocument(999); + expect(result.isErr).toBe(true); + }); + }); + + describe("createDocument", () => { + it("creates a document", async () => { + await seed(dbModule.db, { vehicles }, { count: 1 }); + const file = new File([], "testfile.pdf"); + const result = await createDocument({ + name: "Test Document", + date: new Date(), + mileage: 50000, + note: "Insurance document", + typeId: 1, + file, + entityId: 1, + entityType: "vehicle", + }); + expect(result.isOk).toBe(true); + expect(spy).toHaveBeenCalledWith(file); + const documentsList = await dbModule.db.query.documents.findMany(); + expect(documentsList).toHaveLength(1); + }); + + it("does not create a document if required fields are missing", async () => { + // @ts-expect-error missing property on purpose + const result = await createDocument({ + date: new Date(), + mileage: 50000, + note: "Insurance document", + file: new File([], "testfile.pdf"), + }); + expect(result.isErr).toBe(true); + }); + }); + + describe("updateDocument", () => { + it("updates a document", async () => { + await seed(dbModule.db, { vehicles }, { count: 1 }); + + // Manually create a document and capture its ID + const [{ id: documentId }] = await dbModule.db + .insert(documents) + .values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + }) + .returning({ id: documents.id }); + + const result = await updateDocument(documentId, { + note: "Updated note", + // uri: "https://example.com/updated.pdf", + }); + expect(result.isOk).toBe(true); + const updatedDocument = await dbModule.db.query.documents.findFirst({ + where: { id: documentId }, + }); + expect(updatedDocument?.note).toStrictEqual("Updated note"); + // expect(updatedDocument?.uri).toStrictEqual("https://example.com/updated.pdf"); + }); + + it.todo("returns the correct error type if not found"); + }); + + describe("removeDocument", () => { + it("removes a document", async () => { + await seed(dbModule.db, { vehicles }, { count: 1 }); + + // Manually create documents and capture first ID + const [{ id: documentId }] = await dbModule.db + .insert(documents) + .values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + deleted: false, + }) + .returning({ id: documents.id }); + + for (let i = 0; i < 2; i++) { + await dbModule.db.insert(documents).values({ + name: "Test Document", + typeId: 1, + entityType: "vehicle", + entityId: 1, + deleted: false, + }); + } + + const removeResult = await removeDocument(documentId); + expect(removeResult.isOk).toBe(true); + const result = await listDocuments(); + expect(result.isOk).toBe(true); + if (result.isOk) { + expect(result.value).toHaveLength(2); + } + }); + + it.todo("returns the correct error type if not found"); + }); +}); diff --git a/apps/server/src/modules/documents/service.ts b/apps/server/src/modules/documents/service.ts new file mode 100644 index 0000000..17a3268 --- /dev/null +++ b/apps/server/src/modules/documents/service.ts @@ -0,0 +1,140 @@ +import { db } from "#db/index"; +import { documents } from "#db/schemas/documents"; +import { DbError, NotFoundError } from "#lib/serviceErrors"; +import { saveFile } from "#utils/files"; +import { createDocumentSchema, updateDocumentSchema } from "@cm3k/validation"; +import { eq } from "drizzle-orm"; +import { ok, err } from "true-myth/result"; +import * as z from "zod/v4"; + +const createDocumentServiceSchema = createDocumentSchema.extend({ + entityId: z.number().int(), + entityType: z.enum(["vehicle", "operation"]), +}); + +const toIsoString = (value: Date | null | undefined) => (value ? value.toISOString() : null); + +export const listDocuments = async (entityId?: number, entityType?: "vehicle" | "operation") => { + try { + const whereClause = { + deleted: false, + ...(entityId !== undefined ? { entityId } : {}), + ...(entityType !== undefined ? { entityType } : {}), + }; + const documentsList = await db.query.documents.findMany({ + where: whereClause, + with: { + type: { + columns: { + name: true, + }, + }, + }, + columns: { + id: true, + name: true, + date: true, + mileage: true, + uri: true, + note: true, + }, + }); + return ok( + documentsList.map((document) => ({ + ...document, + date: toIsoString(document.date), + })), + ); + } catch (error) { + return err(new DbError(error)); + } +}; + +export const getDocument = async (id: number) => { + try { + const document = await db.query.documents.findFirst({ + where: { id }, + with: { + type: { + columns: { + name: true, + }, + }, + }, + columns: { + id: true, + name: true, + date: true, + mileage: true, + uri: true, + note: true, + }, + }); + if (document) { + return ok({ + ...document, + date: toIsoString(document.date), + }); + } + return err(new NotFoundError("Document not found")); + } catch (eror) { + return err(new DbError(eror)); + } +}; + +export const createDocument = async (input: z.infer) => { + try { + const { file, date, ...docs } = input; + const dbDate = date ? new Date(date) : undefined; + + const result = await saveFile(file); + if (result.isErr) { + return err(result.error); + } + + const [document] = await db + .insert(documents) + .values({ ...docs, date: dbDate, uri: result.value }) + .returning({ id: documents.id }); + return ok(document.id); + } catch (error) { + return err(new DbError(error)); + } +}; + +export const updateDocument = async (id: number, input: z.infer) => { + try { + const { date, ...rest } = input; + const updateData = { + ...rest, + ...(date !== undefined ? { date: new Date(date) } : {}), + }; + const result = await db + .update(documents) + .set(updateData) + .where(eq(documents.id, id)) + .returning(); + if (result.length === 0) { + return err(new NotFoundError("Document not found")); + } + return ok(); + } catch (error) { + return err(new DbError(error)); + } +}; + +export const removeDocument = async (id: number) => { + try { + const result = await db + .update(documents) + .set({ deleted: true }) + .where(eq(documents.id, id)) + .returning(); + if (result.length === 0) { + return err(new NotFoundError("Document not found")); + } + return ok(); + } catch (error) { + return err(new DbError(error)); + } +}; diff --git a/apps/server/src/modules/operations/router.ts b/apps/server/src/modules/operations/router.ts new file mode 100644 index 0000000..2ad743f --- /dev/null +++ b/apps/server/src/modules/operations/router.ts @@ -0,0 +1,19 @@ +import { listOperations } from "./service"; +import { operationsContract } from "@cm3k/contract"; +import { implement } from "@orpc/server"; + +const o = implement(operationsContract); + +const list = o.operations.list.handler(async () => { + const result = await listOperations(); + if (result.isErr) { + throw result.error; + } + return result.value; +}); + +export const operationsRouter = o.router({ + operations: { + list, + }, +}); diff --git a/apps/server/src/core/operation/service.integration.test.ts b/apps/server/src/modules/operations/service.integration.test.ts similarity index 92% rename from apps/server/src/core/operation/service.integration.test.ts rename to apps/server/src/modules/operations/service.integration.test.ts index 3ca4a3d..50269e0 100644 --- a/apps/server/src/core/operation/service.integration.test.ts +++ b/apps/server/src/modules/operations/service.integration.test.ts @@ -51,12 +51,16 @@ describe("Operations service test", () => { it("returns all operations when no vehicleId is provided", async () => { await seed(dbModule.db, { operations, vehicles }).refine(() => ({ vehicles: { - columns: {}, count: 2, with: { operations: 3, }, }, + operations: { + columns: { + deleted: false, + }, + }, })); const result = await listOperations(); expect(result.isOk).toBe(true); @@ -68,12 +72,17 @@ describe("Operations service test", () => { it("returns operations for a specific vehicle", async () => { await seed(dbModule.db, { operations, vehicles }).refine(() => ({ vehicles: { - columns: {}, + columns: { deleted: false }, count: 2, with: { operations: 3, }, }, + operations: { + columns: { + deleted: false, + }, + }, })); const result = await listOperations(1); expect(result.isOk).toBe(true); @@ -120,7 +129,8 @@ describe("Operations service test", () => { it("creates an operation", async () => { await seed(dbModule.db, { vehicles }, { count: 1 }); const result = await createOperation(1, { - date: new Date("2024-01-15"), + name: "Oil change", + date: "2024-01-15T00:00:00.000Z", mileage: 50000, note: "Oil change", type: "maintenance", @@ -133,7 +143,8 @@ describe("Operations service test", () => { it("does not create an operation if vehicleId is missing", async () => { // @ts-expect-error missing property on purpose const result = await createOperation({ - date: new Date("2024-01-15"), + name: "Oil change", + date: "2024-01-15T00:00:00.000Z", mileage: 50000, note: "Oil change", type: "maintenance", @@ -178,6 +189,11 @@ describe("Operations service test", () => { operations: 3, }, }, + operations: { + columns: { + deleted: false, + }, + }, })); const removeResult = await removeOperation(1); expect(removeResult.isOk).toBe(true); diff --git a/apps/server/src/core/operation/service.ts b/apps/server/src/modules/operations/service.ts similarity index 63% rename from apps/server/src/core/operation/service.ts rename to apps/server/src/modules/operations/service.ts index 17730a6..56bb089 100644 --- a/apps/server/src/core/operation/service.ts +++ b/apps/server/src/modules/operations/service.ts @@ -6,19 +6,31 @@ import { eq } from "drizzle-orm"; import { ok, err } from "true-myth/result"; import * as z from "zod/v4"; +const toIsoString = (value: Date | null | undefined) => (value ? value.toISOString() : null); + export const listOperations = async (vehicleId?: number) => { try { + const whereClause = { + deleted: false, + ...(vehicleId !== undefined ? { vehicleId } : {}), + }; const operationsList = await db.query.operations.findMany({ - where: { vehicleId }, + where: whereClause, columns: { id: true, date: true, + name: true, mileage: true, note: true, type: true, }, }); - return ok(operationsList); + return ok( + operationsList.map((operation) => ({ + ...operation, + date: toIsoString(operation.date), + })), + ); } catch (error) { return err(new DbError(error)); } @@ -31,13 +43,17 @@ export const getOperation = async (id: number) => { columns: { id: true, date: true, + name: true, mileage: true, note: true, type: true, }, }); - if (operation !== undefined) { - return ok(operation); + if (operation) { + return ok({ + ...operation, + date: toIsoString(operation.date), + }); } return err(new NotFoundError("Operation not found")); } catch (eror) { @@ -47,9 +63,11 @@ export const getOperation = async (id: number) => { export const createOperation = async (id: number, input: z.infer) => { try { + const { date, ...rest } = input; + const dbDate = date ? new Date(date) : null; const [operation] = await db .insert(operations) - .values({ ...input, vehicleId: id }) + .values({ ...rest, date: dbDate, vehicleId: id }) .returning({ id: operations.id }); return ok(operation.id); } catch (error) { @@ -59,7 +77,16 @@ export const createOperation = async (id: number, input: z.infer) => { try { - const result = await db.update(operations).set(input).where(eq(operations.id, id)).returning(); + const { date, ...rest } = input; + const updateData = { + ...rest, + ...(date !== undefined ? { date: date ? new Date(date) : null } : {}), + }; + const result = await db + .update(operations) + .set(updateData) + .where(eq(operations.id, id)) + .returning(); if (result.length === 0) { return err(new NotFoundError("Operation not found")); } @@ -71,7 +98,11 @@ export const updateOperation = async (id: number, input: z.infer { try { - const result = await db.delete(operations).where(eq(operations.id, id)).returning(); + const result = await db + .update(operations) + .set({ deleted: true }) + .where(eq(operations.id, id)) + .returning(); if (result.length === 0) { return err(new NotFoundError("Operation not found")); } diff --git a/apps/server/src/modules/vehicles/router.documents.test.ts b/apps/server/src/modules/vehicles/router.documents.test.ts new file mode 100644 index 0000000..8ef9f42 --- /dev/null +++ b/apps/server/src/modules/vehicles/router.documents.test.ts @@ -0,0 +1,135 @@ +import { createDocumentSchema } from "@cm3k/validation"; +import { call, isDefinedError } from "@orpc/server"; +import { ok } from "true-myth/result"; +import { beforeAll, describe, expect, it, vi, type Mocked } from "vitest"; +import { z } from "zod/v4"; + +import { router } from "../../routers"; +import * as documentService from "../documents/service"; + +const expectDefinedError = async (promise: Promise) => { + try { + await promise; + } catch (error) { + const resolved = await error; + expect(isDefinedError(resolved)).toBe(true); + return; + } + + throw new Error("Expected promise to reject"); +}; + +const vehicles = router.vehicles.vehicles; + +describe("/vehicles", () => { + describe("GET /{id}/documents", () => { + let spy: ReturnType; + beforeAll(() => { + spy = vi.spyOn(documentService, "listDocuments"); + }); + + describe("call endpoint with correct arguments", () => { + it("calls listDocuments with correct vehicleId and entityType", async () => { + spy.mockResolvedValue(ok([])); + await call(vehicles.documents.list, { params: { vehicleId: 1 } }); + expect(spy).toHaveBeenCalledWith(1, "vehicle"); + }); + }); + + describe("throw with bad arguments", () => { + it("returns error with invalid vehicleId type", async () => { + await expectDefinedError( + // @ts-expect-error: intentionally passing invalid type for testing + call(vehicles.documents.list, { params: { vehicleId: "invalid" } }), + ); + }); + }); + }); + + describe("POST /vehicles/{id}/documents/", () => { + let spy: Mocked; + beforeAll(() => { + spy = vi + .spyOn(documentService, "createDocument") + .mockResolvedValue(ok(1) as Awaited>); + vi.spyOn(documentService, "getDocument").mockResolvedValue( + ok({ + id: 1, + name: "Test Document", + date: "2024-03-15T00:00:00.000Z", + type: { name: "THing" }, + mileage: 55000, + note: "Insurance document", + uri: "https://example.com/doc.pdf", + }) as Awaited>, + ); + }); + + const mockDocument = { + name: "Test Document", + typeId: 1, + date: new Date(), + mileage: 55000, + note: "Insurance document", + file: new File([], "testfile.pdf"), + }; + + describe("call endpoint with correct arguments", () => { + it("calls createDocument with vehicleId from params and body data", async () => { + await call(vehicles.documents.create, { + body: mockDocument, + params: { vehicleId: 1 }, + }); + expect(spy).toHaveBeenCalledWith({ + ...mockDocument, + entityId: 1, + entityType: "vehicle", + }); + }); + + const required = + z.toJSONSchema(createDocumentSchema, { + unrepresentable: "any", + override: (ctx) => { + const def = ctx.zodSchema._zod.def; + if (def.type === "date") { + ctx.jsonSchema.type = "string"; + ctx.jsonSchema.format = "date-time"; + } + }, + }).required || []; + const optional = Object.keys(mockDocument).filter((k) => !required.includes(k) && k !== "file"); + describe("call endpoint with correct arguments", () => { + it("calls createDocument with correct argument", async () => { + await call(vehicles.documents.create, { + body: mockDocument, + params: { vehicleId: 1 }, + }); + expect(spy).toHaveBeenCalledWith({ + ...mockDocument, + entityId: 1, + entityType: "vehicle", + }); + }); + + it.each(optional)("should not throw without optional property %s", async (a) => { + const damagedDocument = { ...mockDocument, [a]: undefined }; + const result = await call(vehicles.documents.create, { + body: damagedDocument, + params: { vehicleId: 1 }, + }); + expect(result.id).toBe(1); + }); + }); + describe("call endpoint with bad arguments", () => { + it.each(required)("should throw without required property %s", async (a) => { + const damagedDocument = { ...mockDocument, [a]: undefined }; + await expectDefinedError( + // @ts-expect-error: intentionally passing invalid type for testing + call(vehicles.documents.create, damagedDocument), + ); + }); + }); + }); + }); +}); diff --git a/apps/server/src/routers/vehicles.operations.test.ts b/apps/server/src/modules/vehicles/router.operations.test.ts similarity index 63% rename from apps/server/src/routers/vehicles.operations.test.ts rename to apps/server/src/modules/vehicles/router.operations.test.ts index 1eb1fbb..f31ee56 100644 --- a/apps/server/src/routers/vehicles.operations.test.ts +++ b/apps/server/src/modules/vehicles/router.operations.test.ts @@ -1,11 +1,25 @@ -import * as operationService from "#core/operation/service"; import { createOperationSchema } from "@cm3k/validation"; import { call, isDefinedError } from "@orpc/server"; import { ok } from "true-myth/result"; import { beforeAll, describe, expect, it, vi, type Mocked } from "vitest"; import { z } from "zod/v4"; -import { router } from "."; +import { router } from "../../routers"; +import * as operationService from "../operations/service"; + +const expectDefinedError = async (promise: Promise) => { + try { + await promise; + } catch (error) { + const resolved = await error; + expect(isDefinedError(resolved)).toBe(true); + return; + } + + throw new Error("Expected promise to reject"); +}; + +const vehicles = router.vehicles.vehicles; describe("/vehicles", () => { describe("GET /{id}/operations", () => { @@ -17,45 +31,50 @@ describe("/vehicles", () => { describe("call endpoint with correct arguments", () => { it("calls listOperations with correct vehicleId", async () => { spy.mockResolvedValue(ok([])); - await call(router.vehicles.vehicles.operations.list, { params: { vehicleId: 1 } }); + await call(vehicles.operations.list, { params: { vehicleId: 1 } }); expect(spy).toHaveBeenCalledWith(1); }); }); describe("throw with bad arguments", () => { it("returns error with invalid vehicleId type", async () => { - await expect( + await expectDefinedError( // @ts-expect-error: intentionally passing invalid type for testing - call(router.vehicles.vehicles.operations.list, { params: { vehicleId: "invalid" } }), - ).rejects.toSatisfy((err) => isDefinedError(err)); + call(vehicles.operations.list, { params: { vehicleId: "invalid" } }), + ); }); }); }); describe("POST /vehicles/{id}/operations/", () => { let spy: Mocked; - let getOperationSpy: Mocked; beforeAll(() => { - spy = vi.spyOn(operationService, "createOperation").mockResolvedValue(ok(1) as Awaited>); - getOperationSpy = vi.spyOn(operationService, "getOperation").mockResolvedValue(ok({ - id: 1, - date: new Date("2024-03-15"), - type: "maintenance", - mileage: 55000, - note: "Regular maintenance", - }) as Awaited>); + spy = vi + .spyOn(operationService, "createOperation") + .mockResolvedValue(ok(1) as Awaited>); + vi.spyOn(operationService, "getOperation").mockResolvedValue( + ok({ + id: 1, + date: "2024-03-15T00:00:00.000Z", + name: "Regular maintenance", + type: "maintenance", + mileage: 55000, + note: "Regular maintenance", + }) as Awaited>, + ); }); const mockOperation = { + name: "Regular maintenance", type: "maintenance", - date: new Date("2024-03-15"), + date: "2024-03-15T00:00:00.000Z", mileage: 55000, note: "Regular maintenance", }; describe("call endpoint with correct arguments", () => { it("calls createOperation with vehicleId from params and body data", async () => { - await call(router.vehicles.vehicles.operations.create, { + await call(vehicles.operations.create, { body: mockOperation, params: { vehicleId: 1 }, }); @@ -78,7 +97,7 @@ describe("/vehicles", () => { const optional = Object.keys(mockOperation).filter((k) => !required.includes(k)); describe("call endpoint with correct arguments", () => { it("calls createVehicle with correct argument", async () => { - await call(router.vehicles.vehicles.operations.create, { + await call(vehicles.operations.create, { body: mockOperation, params: { vehicleId: 1 }, }); @@ -87,7 +106,7 @@ describe("/vehicles", () => { it.each(optional)("should not throw without optional property %s", async (a) => { const damagedVehicle = { ...mockOperation, [a]: undefined }; - const result = await call(router.vehicles.vehicles.operations.create, { + const result = await call(vehicles.operations.create, { body: damagedVehicle, params: { vehicleId: 1 }, }); @@ -97,10 +116,10 @@ describe("/vehicles", () => { describe("call enpoint with bad arguments", () => { it.each(required)("should throw without required property %s", async (a) => { const damagedVehicle = { ...mockOperation, [a]: undefined }; - await expect( + await expectDefinedError( // @ts-expect-error: intentionally passing invalid type for testing - call(router.vehicles.vehicles.operations.create, damagedVehicle), - ).rejects.toSatisfy((err) => isDefinedError(err)); + call(vehicles.operations.create, damagedVehicle), + ); }); }); }); @@ -109,22 +128,30 @@ describe("/vehicles", () => { describe("DELETE /vehicles/operations/{operationId}", () => { let spy: Mocked; beforeAll(() => { - spy = vi.spyOn(operationService, "removeOperation").mockResolvedValue(ok() as Awaited>); + spy = vi + .spyOn(operationService, "removeOperation") + .mockResolvedValue(ok() as Awaited>); }); describe("call endpoint with correct arguments", () => { it("calls removeOperation with correct id", async () => { - await call(router.vehicles.vehicles.operations.remove, { id: 1 }); + await call(vehicles.operations.remove, { + params: { vehicleId: 1, id: 1 }, + }); expect(spy).toHaveBeenCalledWith(1); }); }); describe("call endpoint with bad arguments", () => { it("rejects with invalid id type", async () => { - await expect( - // @ts-expect-error: intentionally passing invalid type for testing - call(router.vehicles.vehicles.operations.remove, { id: "invalid" }), - ).rejects.toSatisfy((err) => isDefinedError(err)); + await expectDefinedError( + call(vehicles.operations.remove, { + params: { + vehicleId: 1, + id: "invalid", + }, + }), + ); }); }); }); diff --git a/apps/server/src/routers/vehicles.test.ts b/apps/server/src/modules/vehicles/router.test.ts similarity index 72% rename from apps/server/src/routers/vehicles.test.ts rename to apps/server/src/modules/vehicles/router.test.ts index 3bd19a5..1a418c2 100644 --- a/apps/server/src/routers/vehicles.test.ts +++ b/apps/server/src/modules/vehicles/router.test.ts @@ -1,4 +1,4 @@ -import * as vehicleService from "#core/vehicle/service"; +import * as vehicleService from "./service"; import { NotFoundError } from "#lib/serviceErrors"; import { createVehicleSchema, getVehicleSchema, updateVehicleSchema } from "@cm3k/validation"; import { call, isDefinedError } from "@orpc/server"; @@ -6,17 +6,33 @@ import { err, ok } from "true-myth/result"; import { beforeAll, describe, expect, it, vi, type Mocked } from "vitest"; import * as z from "zod/v4"; -import { router } from "."; +import { router } from "../../routers"; + +const expectDefinedError = async (promise: Promise) => { + try { + await promise; + } catch (error) { + const resolved = await error; + expect(isDefinedError(resolved)).toBe(true); + return; + } + + throw new Error("Expected promise to reject"); +}; + +const vehicles = router.vehicles.vehicles; describe("/vehicles", () => { describe("GET /", () => { let spy: Mocked; beforeAll(() => { - spy = vi.spyOn(vehicleService, "listVehicle").mockResolvedValue(ok([]) as Awaited>); + spy = vi + .spyOn(vehicleService, "listVehicle") + .mockResolvedValue(ok([]) as Awaited>); }); it("calls listVehicle", async () => { - await call(router.vehicles.vehicles.list, {}); + await call(vehicles.list, {}); expect(spy).toHaveBeenCalled(); }); }); @@ -32,7 +48,6 @@ describe("/vehicles", () => { trim: "MG", year: 2008, id: 1, - operations: [], }; // @ts-expect-error: intentionally passing invalid type for testing spy = vi.spyOn(vehicleService, "getVehicle").mockResolvedValue(ok(mockVehicle)); @@ -40,7 +55,7 @@ describe("/vehicles", () => { describe("call endpoint with correct arguments", () => { it("calls getVehicle with correct argument", async () => { - await call(router.vehicles.vehicles.get, { id: 12 }); + await call(vehicles.get, { id: 12 }); expect(spy).toHaveBeenCalledWith(12); }); }); @@ -49,9 +64,7 @@ describe("/vehicles", () => { it("return error with bad argument", async () => { // TODO: good place to start typed error checking // @ts-expect-error: intentionally passing invalid type for testing - await expect(call(router.vehicles.vehicles.get, { id: "hello" })).rejects.toSatisfy((err) => - isDefinedError(err), - ); + await expectDefinedError(call(vehicles.get, { id: "hello" })); }); it("return error with unknown id", async () => { @@ -60,9 +73,7 @@ describe("/vehicles", () => { .spyOn(vehicleService, "getVehicle") // @ts-expect-error: intentionally passing invalid type for testing .mockResolvedValue(err(new NotFoundError({ data: { message: "error" } }))); - await expect(call(router.vehicles.vehicles.get, { id: 12 })).rejects.toSatisfy((err) => - isDefinedError(err), - ); + await expectDefinedError(call(vehicles.get, { id: 12 })); }); }); }); @@ -78,38 +89,35 @@ describe("/vehicles", () => { }; let spy: Mocked; - let getVehicleSpy: Mocked; beforeAll(() => { spy = vi .spyOn(vehicleService, "createVehicle") .mockResolvedValue(ok(1) as Awaited>); - getVehicleSpy = vi - .spyOn(vehicleService, "getVehicle") - .mockResolvedValue( - ok({ ...mockVehicle, id: 1, operations: [] }) as Awaited>, - ); + vi.spyOn(vehicleService, "getVehicle").mockResolvedValue( + ok({ ...mockVehicle, id: 1, operations: [] }) as Awaited< + ReturnType + >, + ); }); const required = z.toJSONSchema(createVehicleSchema).required || []; const optional = Object.keys(mockVehicle).filter((k) => !required.includes(k)); describe("call endpoint with correct arguments", () => { it("calls createVehicle with correct argument", async () => { - await call(router.vehicles.vehicles.create, mockVehicle); + await call(vehicles.create, mockVehicle); expect(spy).toHaveBeenCalledWith(mockVehicle); }); it.each(optional)("should not throw without optional property %s", async (a) => { const damagedVehicle = { ...mockVehicle, [a]: undefined }; - const result = await call(router.vehicles.vehicles.create, damagedVehicle); + const result = await call(vehicles.create, damagedVehicle); expect(result.id).toBe(1); }); }); describe("call enpoint with bad arguments", () => { it.each(required)("should throw without required property %s", async (a) => { const damagedVehicle = { ...mockVehicle, [a]: undefined }; - await expect(call(router.vehicles.vehicles.create, damagedVehicle)).rejects.toSatisfy( - (err) => isDefinedError(err), - ); + await expectDefinedError(call(vehicles.create, damagedVehicle)); }); }); }); @@ -126,29 +134,26 @@ describe("/vehicles", () => { }; let spy: Mocked; - let getVehicleSpy: Mocked; beforeAll(() => { - spy = vi - .spyOn(vehicleService, "updateVehicle") - .mockResolvedValue(ok()); - getVehicleSpy = vi - .spyOn(vehicleService, "getVehicle") - .mockResolvedValue( - ok({ ...mockVehicle, id: 1, operations: [] }) as Awaited>, - ); + spy = vi.spyOn(vehicleService, "updateVehicle").mockResolvedValue(ok()); + vi.spyOn(vehicleService, "getVehicle").mockResolvedValue( + ok({ ...mockVehicle, id: 1, operations: [] }) as Awaited< + ReturnType + >, + ); }); const required = z.toJSONSchema(updateVehicleSchema).required || []; const optional = Object.keys(mockVehicle).filter((k) => !required.includes(k)); describe("call endpoint with correct arguments", () => { it("calls createVehicle with correct argument", async () => { - await call(router.vehicles.vehicles.update, { body: mockVehicle, params: { id: 1 } }); + await call(vehicles.update, { body: mockVehicle, params: { id: 1 } }); expect(spy).toHaveBeenCalledWith(1, mockVehicle); }); it.each(optional)("should not throw without optional property %s", async (a) => { const damagedVehicle = { ...mockVehicle, [a]: undefined }; - const result = await call(router.vehicles.vehicles.update, { + const result = await call(vehicles.update, { body: damagedVehicle, params: { id: 1 }, }); @@ -158,7 +163,7 @@ describe("/vehicles", () => { describe("call enpoint with bad arguments", () => { it("should throw throw with id change", async () => { const damagedVehicle = { ...mockVehicle, id: 2, random: "ishouldnotbehere" }; - await call(router.vehicles.vehicles.update, { body: damagedVehicle, params: { id: 1 } }); + await call(vehicles.update, { body: damagedVehicle, params: { id: 1 } }); expect(spy).toHaveBeenCalledWith(1, mockVehicle); }); }); @@ -171,7 +176,7 @@ describe("/vehicles", () => { }); describe("call endpoint with correct arguments", () => { it("calls createVehicle with correct argument", async () => { - await call(router.vehicles.vehicles.remove, { id: 1 }); + await call(vehicles.remove, { id: 1 }); expect(spy).toHaveBeenCalledWith(1); }); }); diff --git a/apps/server/src/routers/vehicles.ts b/apps/server/src/modules/vehicles/router.ts similarity index 69% rename from apps/server/src/routers/vehicles.ts rename to apps/server/src/modules/vehicles/router.ts index 51bc859..591ab1e 100644 --- a/apps/server/src/routers/vehicles.ts +++ b/apps/server/src/modules/vehicles/router.ts @@ -1,16 +1,11 @@ +import { createDocument, getDocument, listDocuments } from "../documents/service"; import { createOperation, getOperation, listOperations, removeOperation, -} from "#core/operation/service"; -import { - createVehicle, - getVehicle, - listVehicle, - removeVehicle, - updateVehicle, -} from "#core/vehicle/service"; +} from "../operations/service"; +import { createVehicle, getVehicle, listVehicle, removeVehicle, updateVehicle } from "./service"; import { vehiclesContract } from "@cm3k/contract"; import { implement } from "@orpc/server"; @@ -64,9 +59,6 @@ const remove = o.vehicles.remove.handler(async ({ input }) => { if (result.isErr) { throw result.error; } - return { - ok: true, - }; }); const operations = { @@ -89,13 +81,36 @@ const operations = { return result.value; }), remove: o.vehicles.operations.remove.handler(async ({ input }) => { - const result = await removeOperation(input.id); + const result = await removeOperation(input.params.id); if (result.isErr) { throw result.error; } - return { - ok: true, + }), +}; + +const documents = { + create: o.vehicles.documents.create.handler(async ({ input }) => { + const documentData = { + ...input.body, + entityId: input.params.vehicleId, + entityType: "vehicle" as const, }; + const id = await createDocument(documentData); + if (id.isErr) { + throw id.error; + } + const result = await getDocument(id.value); + if (result.isErr) { + throw result.error; + } + return result.value; + }), + list: o.vehicles.documents.list.handler(async ({ input }) => { + const result = await listDocuments(input.params.vehicleId, "vehicle"); + if (result.isErr) { + throw result.error; + } + return result.value; }), }; @@ -105,6 +120,7 @@ export const vehiclesRouter = o.router({ get, list, operations, + documents, remove, update, }, diff --git a/apps/server/src/core/vehicle/service.integration.test.ts b/apps/server/src/modules/vehicles/service.integration.test.ts similarity index 80% rename from apps/server/src/core/vehicle/service.integration.test.ts rename to apps/server/src/modules/vehicles/service.integration.test.ts index 5d85c68..0d35cab 100644 --- a/apps/server/src/core/vehicle/service.integration.test.ts +++ b/apps/server/src/modules/vehicles/service.integration.test.ts @@ -3,7 +3,7 @@ import { relations } from "#db/schemas/relations"; import { vehicles } from "#db/schemas/vehicle"; import { NotFoundError } from "#lib/serviceErrors"; import { rootDir } from "#utils/paths"; -import { getVehicleSchema, listVehiclesSchema } from "@cm3k/validation"; +import { listVehiclesSchema } from "@cm3k/validation"; import { drizzle } from "drizzle-orm/libsql"; import { migrate } from "drizzle-orm/libsql/migrator"; import { reset, seed } from "drizzle-seed"; @@ -43,7 +43,13 @@ describe("Vehicles service test", () => { }); it("returns validated vehicles", async () => { - await seed(dbModule.db, { vehicles }, { count: 2 }); + await seed(dbModule.db, { vehicles }, { count: 2 }).refine(() => ({ + vehicles: { + columns: { + deleted: false, + }, + }, + })); const result = await listVehicle(); assert(result.isOk); expect(result.value).toHaveLength(2); @@ -52,35 +58,6 @@ describe("Vehicles service test", () => { }); describe("getVehicle", () => { - it("returns a vehicle with operations", async () => { - await seed(dbModule.db, { operations, vehicles }).refine(() => ({ - vehicles: { - columns: {}, - count: 1, - with: { - operations: 10, - }, - }, - })); - const result = await getVehicle(1); - assert(result.isOk); - expect(result.value.operations.length).toEqual(10); - expect(getVehicleSchema.safeParse(result.value).error).toBeUndefined(); - }); - - it("returns a vehicle without operations", async () => { - await seed(dbModule.db, { vehicles }).refine(() => ({ - vehicles: { - columns: {}, - count: 1, - }, - })); - const result = await getVehicle(1); - assert(result.isOk); - expect(result.value.operations.length).toEqual(0); - expect(getVehicleSchema.safeParse(result.value).error).toBeUndefined(); - }); - it("returns the correct error type if not found", async () => { await seed(dbModule.db, { vehicles }).refine(() => ({ vehicles: { @@ -126,7 +103,7 @@ describe("Vehicles service test", () => { it("update a vehicle", async () => { await seed(dbModule.db, { operations, vehicles }).refine(() => ({ vehicles: { - columns: {}, + columns: { deleted: false }, count: 1, with: { operations: 10, @@ -145,7 +122,12 @@ describe("Vehicles service test", () => { describe("removeVehicle", () => { it("remove a vehicle", async () => { - await seed(dbModule.db, { vehicles }, { count: 2 }); + await seed(dbModule.db, { operations, vehicles }).refine(() => ({ + vehicles: { + columns: { deleted: false }, + count: 2, + }, + })); const result = await removeVehicle(1); assert(result.isOk); const vehiclesList = await listVehicle(); diff --git a/apps/server/src/core/vehicle/service.ts b/apps/server/src/modules/vehicles/service.ts similarity index 80% rename from apps/server/src/core/vehicle/service.ts rename to apps/server/src/modules/vehicles/service.ts index 6cf8b9d..5c67b55 100644 --- a/apps/server/src/core/vehicle/service.ts +++ b/apps/server/src/modules/vehicles/service.ts @@ -8,7 +8,19 @@ import * as z from "zod/v4"; export const listVehicle = async () => { try { - const vehiclesList = await db.select().from(vehicles); + const vehiclesList = await db.query.vehicles.findMany({ + where: { deleted: false }, + columns: { + id: true, + brand: true, + description: true, + engine: true, + model: true, + power: true, + trim: true, + year: true, + }, + }); return ok(vehiclesList); } catch { return err(new DbError()); @@ -28,17 +40,6 @@ export const getVehicle = async (id: number) => { trim: true, year: true, }, - with: { - operations: { - columns: { - id: true, - date: true, - mileage: true, - note: true, - type: true, - }, - }, - }, }); if (row !== undefined) { return ok(row); @@ -71,7 +72,11 @@ export const updateVehicle = async (id: number, input: z.infer { try { - const result = await db.delete(vehicles).where(eq(vehicles.id, id)).returning(); + const result = await db + .update(vehicles) + .set({ deleted: true }) + .where(eq(vehicles.id, id)) + .returning(); if (result.length === 0) { return err(new NotFoundError("Vehicle not found")); } diff --git a/apps/server/src/openapi/openapi-generate.ts b/apps/server/src/openapi/openapi-generate.ts new file mode 100644 index 0000000..551ffd5 --- /dev/null +++ b/apps/server/src/openapi/openapi-generate.ts @@ -0,0 +1,28 @@ +import { OpenAPIGenerator } from "@orpc/openapi"; +import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4"; +import { writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +import { router } from "../routers"; + +const outputPath = fileURLToPath(new URL("../openapi.json", import.meta.url)); + +const generator = new OpenAPIGenerator({ + schemaConverters: [new ZodToJsonSchemaConverter()], +}); + +try { + const document = await generator.generate(router, { + info: { + title: "CM3K API", + version: "1.0.0", + }, + }); + + await writeFile(outputPath, JSON.stringify(document, null, 2)); + console.log(`OpenAPI document written to ${outputPath}`); +} catch (error) { + console.error("Failed to generate OpenAPI document."); + console.error(error); + process.exitCode = 1; +} diff --git a/apps/server/src/openapi.ts b/apps/server/src/openapi/openapi.ts similarity index 97% rename from apps/server/src/openapi.ts rename to apps/server/src/openapi/openapi.ts index e9a1d5a..a63e237 100644 --- a/apps/server/src/openapi.ts +++ b/apps/server/src/openapi/openapi.ts @@ -7,7 +7,7 @@ import { serveStatic } from "hono/bun"; import { logger } from "hono/logger"; import { absolutePath } from "swagger-ui-dist"; -import { router } from "./routers"; +import { router } from "../routers"; const app = new Hono(); app.use(logger()); diff --git a/apps/server/src/routers/index.ts b/apps/server/src/routers/index.ts index 21ff998..961ad82 100644 --- a/apps/server/src/routers/index.ts +++ b/apps/server/src/routers/index.ts @@ -1,10 +1,14 @@ import { errorMiddleware } from "#middlewares/errorMiddleware"; import { os } from "@orpc/server"; -import { operationsRouter } from "./operations"; -import { vehiclesRouter } from "./vehicles"; +import { documentsRouter } from "../modules/documents/router"; +import { documentTypesRouter } from "../modules/document-types/router"; +import { operationsRouter } from "../modules/operations/router"; +import { vehiclesRouter } from "../modules/vehicles/router"; export const router = os.use(errorMiddleware).router({ operations: operationsRouter, vehicles: vehiclesRouter, + documents: documentsRouter, + documentType: documentTypesRouter, }); diff --git a/apps/server/src/routers/operations.ts b/apps/server/src/routers/operations.ts deleted file mode 100644 index a183328..0000000 --- a/apps/server/src/routers/operations.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { listOperations } from "#core/operation/service"; -import { os } from "@orpc/server"; - -const list = os - .route({ - inputStructure: "detailed", - method: "GET", - path: "/", - }) - .handler(async () => { - const result = await listOperations(); - if (result.isErr) { - throw result.error; - } - return result.value; - }); - -export const operationsRouter = os.prefix("/operations").router({ - list, -}); diff --git a/apps/server/src/utils/files.ts b/apps/server/src/utils/files.ts new file mode 100644 index 0000000..05dfe48 --- /dev/null +++ b/apps/server/src/utils/files.ts @@ -0,0 +1,15 @@ +import { FileError } from "#lib/serviceErrors"; +import { writeFile } from "fs/promises"; +import path from "path"; +import { err, ok } from "true-myth/result"; + +import { rootDir } from "./paths"; +export const saveFile = async (file: File) => { + try { + const data = Buffer.from(await file.arrayBuffer()); + await writeFile(path.join(rootDir, "uploads", file.name), data); + return ok(`/uploads/${file.name}`); + } catch (error) { + return err(new FileError(error)); + } +}; diff --git a/apps/server/src/utils/logger.ts b/apps/server/src/utils/logger.ts index e9850f6..28fae34 100644 --- a/apps/server/src/utils/logger.ts +++ b/apps/server/src/utils/logger.ts @@ -13,7 +13,10 @@ export const httpPino = pino( }), ); -export const logger = pino({ - level: "error", - timestamp: pino.stdTimeFunctions.isoTime, -}); +export const logger = pino( + { + level: "error", + timestamp: pino.stdTimeFunctions.isoTime, + }, + pretty(), +); diff --git a/apps/web/.oxfmtrc.json b/apps/web/.oxfmtrc.json new file mode 100644 index 0000000..8b1001f --- /dev/null +++ b/apps/web/.oxfmtrc.json @@ -0,0 +1,8 @@ +{ + "experimentalTailwindcss": { + "stylesheet": "./src/index.css", + "attributes": ["class", "className"], + "functions": ["clsx", "cn"], + "preserveWhitespace": true + } +} diff --git a/apps/web/package.json b/apps/web/package.json index 37b15df..5fe4f1d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,31 +18,32 @@ "@orpc/contract": "^1.13.4", "@orpc/openapi-client": "^1.13.4", "@orpc/tanstack-query": "^1.13.4", - "@tanstack/react-form": "^1.27.7", - "@tanstack/react-query": "^5.90.19", - "@tanstack/react-router": "^1.153.2", - "@tanstack/react-router-devtools": "^1.153.2", + "@tanstack/react-form": "^1.28.0", + "@tanstack/react-query": "^5.90.20", + "@tanstack/react-router": "^1.158.0", + "@tanstack/react-router-devtools": "^1.158.0", "@tanstack/react-table": "^8.21.3", + "clsx": "^2.1.1", "date-fns": "^4.1.0", - "lucide-react": "^0.545.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", + "lucide-react": "^0.563.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", "react-error-boundary": "^6.1.0", "tailwindcss": "^4.1.18", - "zod": "^4.3.5" + "zod": "^4.3.6" }, "devDependencies": { "@cm3k/server": "workspace:*", "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-query-devtools": "^5.91.2", - "@tanstack/router-plugin": "^1.153.2", - "@types/react": "^19.2.8", + "@tanstack/react-query-devtools": "^5.91.3", + "@tanstack/router-plugin": "^1.158.0", + "@types/react": "^19.2.11", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.2", - "globals": "^16.5.0", - "oxfmt": "^0.21.0", - "oxlint": "^1.41.0", + "@vitejs/plugin-react": "^5.1.3", + "globals": "^17.3.0", + "oxfmt": "^0.28.0", + "oxlint": "^1.43.0", "vite": "^7.3.1", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^6.0.5" } } diff --git a/apps/web/src/components/layout/Main.tsx b/apps/web/src/components/layout/Main.tsx new file mode 100644 index 0000000..424ad23 --- /dev/null +++ b/apps/web/src/components/layout/Main.tsx @@ -0,0 +1,9 @@ +import { Outlet } from "@tanstack/react-router"; + +export default function Main() { + return ( +
+ +
+ ); +} diff --git a/apps/web/src/components/layout/Sidebar/Item.tsx b/apps/web/src/components/layout/Sidebar/Item.tsx new file mode 100644 index 0000000..b7bd62a --- /dev/null +++ b/apps/web/src/components/layout/Sidebar/Item.tsx @@ -0,0 +1,33 @@ +import { createLink, type LinkComponent } from "@tanstack/react-router"; +import { type ReactNode, type RefObject } from "react"; + +interface ItemLinkProps extends React.AnchorHTMLAttributes { + label?: string; + icon?: ReactNode; +} + +const BasicLinkComponent = ({ + ref, + icon, + label, + ...props +}: ItemLinkProps & { ref?: RefObject }) => { + return ( +
  • + + {icon} + {label} + +
  • + ); +}; + +const CreatedLinkComponent = createLink(BasicLinkComponent); + +export const Item: LinkComponent = (props) => { + return ; +}; diff --git a/apps/web/src/components/layout/Sidebar/SectionItem.tsx b/apps/web/src/components/layout/Sidebar/SectionItem.tsx new file mode 100644 index 0000000..031868a --- /dev/null +++ b/apps/web/src/components/layout/Sidebar/SectionItem.tsx @@ -0,0 +1,86 @@ +import { ChevronRight } from "lucide-react"; +import { + useCallback, + useContext, + useEffect, + useState, + type AnchorHTMLAttributes, + type MouseEvent, + type PropsWithChildren, + type ReactNode, +} from "react"; +import { SidebarContext } from "./Sidebar"; +import { createLink, type LinkComponent } from "@tanstack/react-router"; +import { type RefObject } from "react"; + +interface CollapsibleSectionContentProps extends PropsWithChildren> { + label?: string; + icon?: ReactNode; +} + +const CollapsibleSectionContent = ({ + ref, + icon, + label, + children, + onClick, + ...props +}: CollapsibleSectionContentProps & { ref?: RefObject }) => { + const [open, setOpen] = useState(false); + const { collapsed, toggleCollapse } = useContext(SidebarContext); + + const toggleOpen = useCallback(() => { + setOpen((isOpen) => !isOpen); + }, []); + + const handleSectionClick = useCallback( + (e: MouseEvent) => { + if (onClick != null) { + onClick(e); + } + if (collapsed) { + toggleCollapse(); + } + setOpen(true); + }, + [onClick, toggleCollapse, collapsed], + ); + + useEffect(() => { + if (collapsed) { + setOpen(false); + } + }, [collapsed]); + + return ( + <> + + +
      +
      {children}
      +
    + + ); +}; + +const LinkedSectionContent = createLink(CollapsibleSectionContent); + +export const SectionItem: LinkComponent = (props) => { + return ; +}; diff --git a/apps/web/src/components/layout/Sidebar/Sidebar.tsx b/apps/web/src/components/layout/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..03fcc03 --- /dev/null +++ b/apps/web/src/components/layout/Sidebar/Sidebar.tsx @@ -0,0 +1,61 @@ +import { Car, ChevronsLeft, Home, Settings } from "lucide-react"; +import { createContext, useCallback, useContext, useState, type PropsWithChildren } from "react"; +import { Item } from "./Item"; +import { SectionItem } from "./SectionItem"; + +export const SidebarContext = createContext({ + collapsed: false, + setCollapsed: (_collapsed: boolean) => {}, + toggleCollapse: () => {}, +}); + +function SidebarContextProvider({ children }: PropsWithChildren) { + const [collapsed, setCollapsed] = useState(false); + + const toggleCollapse = useCallback(() => { + setCollapsed((isCollapsed) => !isCollapsed); + }, []); + + return ( + + {children} + + ); +} + +function SidebarContent() { + const { collapsed, toggleCollapse } = useContext(SidebarContext); + + return ( + + ); +} + +export default function Sidebar() { + return ( + + + + ); +} + +export const useSidebar = () => useContext(SidebarContext); diff --git a/apps/web/src/components/ui/Button.tsx b/apps/web/src/components/ui/Button.tsx index 0887585..e2dea16 100644 --- a/apps/web/src/components/ui/Button.tsx +++ b/apps/web/src/components/ui/Button.tsx @@ -17,7 +17,7 @@ function Button({ callback, variant = "primary", ...props }: ComponentProps<"but return (