From 4510712359a402c3adcafe1b46533e37e3e891f2 Mon Sep 17 00:00:00 2001 From: tonyaellie Date: Wed, 30 Oct 2024 16:01:05 +0000 Subject: [PATCH 01/15] feat: make 404 follow theme and add a return home page --- frontend/layouts/404.vue | 2 +- frontend/pages/[...all].vue | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/layouts/404.vue b/frontend/layouts/404.vue index 5ee95acc..31e31392 100644 --- a/frontend/layouts/404.vue +++ b/frontend/layouts/404.vue @@ -1,5 +1,5 @@ diff --git a/frontend/pages/[...all].vue b/frontend/pages/[...all].vue index 0ae6f5c4..2fdddf16 100644 --- a/frontend/pages/[...all].vue +++ b/frontend/pages/[...all].vue @@ -8,8 +8,9 @@ From 40b1793cce0c920bc9af2a839aa167fad3ee9b6e Mon Sep 17 00:00:00 2001 From: tonyaellie Date: Wed, 30 Oct 2024 22:40:02 +0000 Subject: [PATCH 02/15] feat: sanitise translations when using v-html --- frontend/pages/tools.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/pages/tools.vue b/frontend/pages/tools.vue index c4597314..6f28cca6 100644 --- a/frontend/pages/tools.vue +++ b/frontend/pages/tools.vue @@ -40,7 +40,7 @@ -
+
@@ -57,7 +57,7 @@ {{ $t("tools.actions") }} @@ -75,13 +75,13 @@ -
+
-
+
@@ -91,6 +91,7 @@ + + \ No newline at end of file From d5e66f29b0050feabb82e73ccca541ec4b5e5dc7 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Thu, 31 Oct 2024 22:01:32 -0400 Subject: [PATCH 04/15] chore: remove try it button from api docs --- docs/en/api.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/api.md b/docs/en/api.md index 9ecce9a3..fc8bae6c 100644 --- a/docs/en/api.md +++ b/docs/en/api.md @@ -22,6 +22,12 @@ if (isDark.value) { } + + Date: Sat, 2 Nov 2024 17:47:12 +0000 Subject: [PATCH 05/15] Translated using Weblate (English) Currently translated at 100.0% (276 of 276 strings) Translated using Weblate (English) Currently translated at 100.0% (274 of 274 strings) Translated using Weblate (English) Currently translated at 100.0% (273 of 273 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (273 of 273 strings) Translated using Weblate (French) Currently translated at 75.8% (207 of 273 strings) Translated using Weblate (French) Currently translated at 75.8% (207 of 273 strings) Translated using Weblate (French) Currently translated at 75.8% (207 of 273 strings) Translated using Weblate (French) Currently translated at 75.8% (207 of 273 strings) Translated using Weblate (French) Currently translated at 65.9% (180 of 273 strings) Co-authored-by: 101br03k Co-authored-by: Matthew Kilgore Co-authored-by: MyMemory Co-authored-by: Weblate Co-authored-by: buzz Co-authored-by: mcarbonne Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/en/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/ Translation: Homebox/Frontend --- frontend/locales/en.json | 4 ++++ frontend/locales/fr.json | 41 ++++++++++++++++++++++++++++++++++++++-- frontend/locales/nl.json | 6 +++--- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 6835a83a..c18480d8 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -225,6 +225,10 @@ "zh-MO": "Chinese (Macau)", "zh-TW": "Chinese (Traditional)" }, + "languages.da-DK": "Danish", + "languages.fi.FI": "Finnish", + "languages.ro-RO": "Romanian", + "languages.sk-SK": "Slovak", "locations": { "child_locations": "Child Locations", "collapse_tree": "Collapse Tree", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index fa1d9273..d743c87b 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -9,6 +9,30 @@ } }, "global": { + "date_time": { + "ago": "Il y a {0}", + "days": "jours", + "hour": "heure", + "hours": "heures", + "in": "autour de {0}", + "just-now": "juste maintenant", + "last-month": "Le mois précédent", + "last-week": "la semaine dernière", + "last-year": "l'année dernière", + "minute": "minute", + "minutes": "minutes", + "months": "mois", + "next-month": "Le mois prochain", + "next-week": "la semaine prochaine", + "next-year": "l'année prochaine", + "second": "seconde", + "seconds": "secondes", + "tomorrow": "demain", + "week": "semaine", + "weeks": "semaines", + "years": "années", + "yesterday": "hier" + }, "page_qr_code": { "page_url": "URL de la page" }, @@ -18,6 +42,8 @@ }, "item": { "create_modal": { + "item_description": "Description de l'article", + "item_name": "Nom de l'article", "photo_button": "Photo 📷", "title": "Créer un article" }, @@ -27,25 +53,36 @@ "items": "Articles", "no_items": "Pas d'articles à afficher", "table": "Tableau" + }, + "table": { + "page": "Page", + "rows_per_page": "Lignes par page" } } }, "label": { "create_modal": { + "label_description": "Description de l'étiquette", + "label_name": "Nom de l'étiquette", "title": "Créer une étiquette" } }, "location": { "create_modal": { + "location_description": "Description de l'emplacement", + "location_name": "Nom de l'emplacement", "title": "Créer un emplacement" }, + "selector": { + "parent_location": "Emplacement parent" + }, "tree": { - "no_locations": "Aucun emplacement disponible. Ajoutez votre premiers emplacements avec\nle bouton `<`span class=\"link-primary\"`>`Créer`<`/span`>` dans la barre de navigation." + "no_locations": "Aucun emplacement disponible. Ajoutez votre premier emplacement avec\nle bouton `<`span class=\"link-primary\"`>`Créer`<`/span`>` dans la barre de navigation." } } }, "global": { - "build": "Assemblage : { build }", + "build": "Version : { build }", "confirm": "Confirmer", "create": "Créer", "create_and_add": "Créer et en ajouter un autre", diff --git a/frontend/locales/nl.json b/frontend/locales/nl.json index 4ace1a2e..95bcbc08 100644 --- a/frontend/locales/nl.json +++ b/frontend/locales/nl.json @@ -70,7 +70,7 @@ "location": { "create_modal": { "location_description": "Locatie omschrijving", - "location_name": "Locatie", + "location_name": "Locatie Naam", "title": "Maak locatie" }, "selector": { @@ -289,7 +289,7 @@ "example": "Voorbeeld", "gen_invite": "Genereer Uitnodigingslink", "group_settings": "Groeps Instellingen", - "group_settings_sub": "Gedeelde groepsinstellingen. Het kan zijn dat je je browser moet verversen om alle instellingen te zien werken.", + "group_settings_sub": "Gedeelde groepsinstellingen. Het kan zijn dat je de browser moet verversen om alle instellingen te zien werken.", "inactive": "Inactief", "language": "Taal", "new_password": "Nieuw Wachtwoord", @@ -299,7 +299,7 @@ "notifiers_sub": "Krijg notificaties voor opkomende onderhouds herinneringen", "test": "Test", "theme_settings": "Theme instellingen", - "theme_settings_sub": "Thema-instellingen worden opgeslagen in de lokale opslag van uw browser. Je kan deze wijzigen op elk moment. \nAls je problemen hebt met de instellingen kun je je browser verversen.", + "theme_settings_sub": "Thema-instellingen worden opgeslagen in de lokale opslag van uw browser. Je kan deze wijzigen op elk moment. \nAls je problemen hebt met de instellingen kun je de browser verversen.", "update_group": "Groep bijwerken", "update_language": "Taal bijwerken", "url": "URL", From 7228c64b26f377541ceba8b1ff7db42bb3079b2b Mon Sep 17 00:00:00 2001 From: mcarbonne <46689813+mcarbonne@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:23:35 +0100 Subject: [PATCH 06/15] change link to aim maintenance page of item (#270) --- frontend/components/Maintenance/ListView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/Maintenance/ListView.vue b/frontend/components/Maintenance/ListView.vue index da87230a..71664321 100644 --- a/frontend/components/Maintenance/ListView.vue +++ b/frontend/components/Maintenance/ListView.vue @@ -131,7 +131,7 @@ - + {{ (e as MaintenanceEntryWithDetails).itemName }} - From b5c7566d3782d08f78d7cf817d82f02a874655b9 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 5 Nov 2024 16:29:39 +0000 Subject: [PATCH 07/15] Translated using Weblate (Chinese (Simplified)) Currently translated at 97.1% (269 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 97.1% (269 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 97.1% (269 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 78.3% (217 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 78.3% (217 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 78.3% (217 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 63.8% (177 of 277 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 63.8% (177 of 277 strings) Translated using Weblate (French) Currently translated at 76.1% (211 of 277 strings) Translated using Weblate (French) Currently translated at 76.1% (211 of 277 strings) Translated using Weblate (Italian) Currently translated at 100.0% (277 of 277 strings) Translated using Weblate (Italian) Currently translated at 100.0% (277 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 96.0% (266 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 96.0% (266 of 277 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (277 of 277 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (277 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 92.0% (255 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 92.0% (255 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 91.6% (254 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 91.6% (254 of 277 strings) Translated using Weblate (Hungarian) Currently translated at 91.6% (254 of 277 strings) Translated using Weblate (Slovenian) Currently translated at 100.0% (277 of 277 strings) Translated using Weblate (Danish) Currently translated at 8.3% (23 of 277 strings) Translated using Weblate (Danish) Currently translated at 8.3% (23 of 277 strings) Co-authored-by: 101br03k Co-authored-by: Adam Kleizer Co-authored-by: Frederik Andersen Co-authored-by: Jackxwb Co-authored-by: MyMemory Co-authored-by: Weblate Co-authored-by: Weblate Translation Memory Co-authored-by: alexdelli Co-authored-by: buzz Co-authored-by: thehijacker Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/da/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/fr/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/hu/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/it/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/nl/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/sl/ Translate-URL: https://translate.sysadminsmedia.com/projects/homebox/frontend/zh_Hans/ Translation: Homebox/Frontend --- frontend/locales/da-DK.json | 17 ++++++ frontend/locales/fr.json | 4 ++ frontend/locales/hu.json | 104 +++++++++++++++++++++++++++++++++- frontend/locales/it.json | 4 ++ frontend/locales/nl.json | 4 ++ frontend/locales/sl.json | 4 ++ frontend/locales/zh-CN.json | 110 +++++++++++++++++++++++++++++++++++- 7 files changed, 241 insertions(+), 6 deletions(-) diff --git a/frontend/locales/da-DK.json b/frontend/locales/da-DK.json index 50c9b351..a8802d03 100644 --- a/frontend/locales/da-DK.json +++ b/frontend/locales/da-DK.json @@ -1,6 +1,19 @@ { "components": { + "app": { + "import_dialog": { + "title": "Importer CSV Fil", + "upload": "Upload" + } + }, "global": { + "date_time": { + "just-now": "lige nu", + "last-month": "sidste måned" + }, + "page_qr_code": { + "page_url": "Side URL" + }, "password_score": { "password_strength": "Adgangskodestyrke" } @@ -16,6 +29,9 @@ "items": "Genstande", "no_items": "Ingen genstande at vise", "table": "Tabel" + }, + "table": { + "page": "Side" } } }, @@ -38,6 +54,7 @@ "create": "Opret", "create_and_add": "Opret og tilføj ny", "created": "Oprettet", + "delete": "Slet", "email": "Email", "follow_dev": "Følg udvikleren", "github": "GitHub projekt" diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index d743c87b..84b5140f 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -164,6 +164,10 @@ "zh-MO": "Chinois (Macao)", "zh-TW": "Chinois (traditionnel)" }, + "languages.da-DK": "Danois", + "languages.fi.FI": "Finnois", + "languages.ro-RO": "Roumain", + "languages.sk-SK": "Slovaque", "locations": { "no_results": "Aucun emplacement trouvé" }, diff --git a/frontend/locales/hu.json b/frontend/locales/hu.json index 50e0cc9d..f422be99 100644 --- a/frontend/locales/hu.json +++ b/frontend/locales/hu.json @@ -9,6 +9,19 @@ } }, "global": { + "date_time": { + "ago": "{0} ezelőtt", + "in": "{0} múlva", + "just-now": "épp most", + "last-month": "múlt hónapban", + "last-week": "múlt héten", + "last-year": "tavaly", + "next-month": "jövő hónapban", + "next-week": "jövő héten", + "next-year": "jövőre", + "tomorrow": "holnap", + "yesterday": "tegnap" + }, "page_qr_code": { "page_url": "Oldal URL-je" }, @@ -18,6 +31,8 @@ }, "item": { "create_modal": { + "item_description": "Tétel leírása", + "item_name": "Tétel neve", "photo_button": "Fénykép 📷", "title": "Új elem létrehozása" }, @@ -27,29 +42,45 @@ "items": "Tételek", "no_items": "Nincs megjeleníthető elem", "table": "Táblázat" + }, + "table": { + "page": "Oldal", + "rows_per_page": "Sorok oldalanként" } } }, "label": { "create_modal": { + "label_description": "Címke leírása", + "label_name": "Címke neve", "title": "Címke létrehozása" } }, "location": { "create_modal": { + "location_description": "Hely leírása", + "location_name": "Hely neve", "title": "Új hely létrehozása" }, + "selector": { + "parent_location": "Ezt tartalmazó hely" + }, "tree": { "no_locations": "Nincs elérhető hely. Adj hozzá új helyet a\n `<`span class=\"link-primary\"`>`Létrehozás`<`/span`>` gombbal a navigációs sávon." } } }, "global": { + "add": "Hozzáadás", "build": "Build: { build }", "confirm": "Megerősítés", "create": "Létrehozás", "create_and_add": "Létrehozás és újabb hozzáadása", "created": "Létrehozva", + "delete": "Törlés", + "details": "Részletek", + "duplicate": "Másolás", + "edit": "Szerkesztés", "email": "Email", "follow_dev": "Kövesd a fejlesztőt", "github": "Github projekt", @@ -57,15 +88,29 @@ "join_discord": "Csatlakozz a Discordhoz", "labels": "Címkék", "locations": "Helyek", + "maintenance": "Karbantartás", "name": "Név", "password": "Jelszó", "read_docs": "Olvasd el a dokumentációt", + "save": "Mentés", "search": "Keresés", "sign_out": "Kijelentkezés", "submit": "Elküldés", + "update": "Módosítás", + "value": "Érték", "version": "Verzió: { version }", "welcome": "Üdv, { username }" }, + "home": { + "labels": "Címkék", + "quick_statistics": "Gyors statisztika", + "recently_added": "Nemrég hozzáadott", + "storage_locations": "Tárolási helyek", + "total_items": "Összes tétel", + "total_labels": "Összes címke", + "total_locations": "Összes hely", + "total_value": "Teljes érték" + }, "index": { "disabled_registration": "Regisztráció kikapcsolva", "dont_join_group": "Nem szeretnél csatlakozni egy csoporthoz?", @@ -80,32 +125,71 @@ }, "items": { "add": "Hozzáadás", + "advanced": "Haladó", + "archived": "Archivált", + "asset_id": "Eszközazonosító", + "attachment": "Melléklet", + "attachments": "Mellékletek", + "changes_persisted_immediately": "A mellékletek módosításai azonnal mentésre kerülnek", "created_at": "Létrehozás dátuma", "custom_fields": "Egyedi mezők", + "description": "Leírás", + "details": "Részletek", + "drag_and_drop": "Húzd ide a fájlokat, vagy kattints a fájlok kiválasztásához", + "edit_details": "Részletek szerkesztése", "field_selector": "Mezőválasztó", "field_value": "Mező értéke", "first": "Első", "include_archive": "Archivált elemek belefoglalása", + "insured": "Biztosítva", "last": "Utolsó", + "lifetime_warranty": "Élettartam garancia", + "location": "Hely", + "manual": "Kézikönyv", + "manuals": "Kézikönyvek", + "manufacturer": "Gyártó", + "model_number": "Modellszám", + "name": "Név", "negate_labels": "Címkeválasztás negálása", "next_page": "Következő oldal", "no_results": "Egy elem sem található", + "notes": "Megjegyzések", "options": "Opciók", "order_by": "Rendezés", "pages": "{page}/{totalPages} oldal", + "parent_item": "Szülő tétel", + "photo": "Fénykép", + "photos": "Fényképek", "prev_page": "Előző oldal", + "purchase_date": "Vásárlás dátuma", + "purchase_details": "Vásárlás részletei", + "purchase_price": "Beszerzési ár", + "purchased_from": "Beszerzési hely", + "quantity": "Mennyiség", "query_id": "Eszközazonosító szám lekérdezése: { id }", + "receipt": "Számla", + "receipts": "Számlák", "reset_search": "Alaphelyzet", "results": "{total} találat", + "serial_number": "Sorozatszám", + "show_advanced_view_options": "További beállítások megjelenítése", + "sold_at": "Eladás dátuma", + "sold_details": "Eladás részletei", + "sold_price": "Eladási ár", + "sold_to": "Vevő", "tip_1": "A hely- és címkeszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.", "tip_2": "A '#' előtaggal ellátott keresések egy eszközazonosítót fognak lekérdezni (például '#000-001')", "tip_3": "A mezőszűrők a „vagy” műveletet használják. Ha egynél többet választasz ki,\n bármelyik egyezése esetén megjelenik a tétel.", "tips": "Tippek", "tips_sub": "Tippek a kereséshez", - "updated_at": "Változtatás dátuma" + "updated_at": "Változtatás dátuma", + "warranty": "Garancia", + "warranty_details": "Garancia részletei", + "warranty_expires": "Garancia vége" }, "labels": { - "no_results": "Nem található címke" + "no_results": "Nem található címke", + "update_label": "Címke módosítása" }, "languages": { "ca": "Katalán", @@ -115,20 +199,30 @@ "fr": "Francia", "hu": "Magyar", "it": "Olasz", + "ja-JP": "Japán", "nl": "Holland", "pl": "Lengyel", "pt-BR": "Portugál (brazíliai)", + "pt-PT": "Portugál (Portugália)", "ru": "Orosz", "sl": "Szlovén", "sv": "Svéd", "tr": "Török", + "uk-UA": "Ukrán", "zh-CN": "Kínai (egyszerűsített)", "zh-HK": "Kínai (hongkongi)", "zh-MO": "Kínai (makaói)", "zh-TW": "Kínai (hagyományos)" }, + "languages.da-DK": "Dán", + "languages.fi.FI": "Finn", + "languages.ro-RO": "Román", + "languages.sk-SK": "Szlovák", "locations": { - "no_results": "Nem található hely" + "child_locations": "Tartalmazott helyek", + "collapse_tree": "Fanézet becsukása", + "no_results": "Nem található hely", + "update_location": "Hely módosítása" }, "maintenance": { "filter": { @@ -166,6 +260,9 @@ "total_entries": "Összes bejegyzés" }, "menu": { + "create_item": "Tétel / Eszköz", + "create_label": "Címke", + "create_location": "Hely", "home": "Kezdőlap", "locations": "Helyek", "maintenance": "Karbantartás", @@ -182,6 +279,7 @@ "delete_account_sub": "Törlöd a fiókodat és az összes kapcsolódó adatot. Ezt a műveletet nem lehet visszavonni.", "display_header": "{ currentValue, select, true {Fejléc elrejtése} false {Fejléc megjelenítése} other{Nincs találat}}", "enabled": "Engedélyezve", + "example": "Példa", "gen_invite": "Meghívó link létrehozása", "group_settings": "Csoport beállításai", "group_settings_sub": "Ezek a csoport közös beállításai. Lehet hogy frissítened kell az oldalt a böngésződben a beállítások megváltoztatása után.", diff --git a/frontend/locales/it.json b/frontend/locales/it.json index 76c8bdd0..6345e112 100644 --- a/frontend/locales/it.json +++ b/frontend/locales/it.json @@ -225,6 +225,10 @@ "zh-MO": "Cinese (Macao)", "zh-TW": "Cinese (tradizionale)" }, + "languages.da-DK": "Danese", + "languages.fi.FI": "Finlandese", + "languages.ro-RO": "Rumeno", + "languages.sk-SK": "Slovacco", "locations": { "child_locations": "Ubicazione figlia", "collapse_tree": "Contrai albero", diff --git a/frontend/locales/nl.json b/frontend/locales/nl.json index 95bcbc08..b09d61d0 100644 --- a/frontend/locales/nl.json +++ b/frontend/locales/nl.json @@ -225,6 +225,10 @@ "zh-MO": "Chinees (Macau)", "zh-TW": "Chinees (traditioneel)" }, + "languages.da-DK": "Deens", + "languages.fi.FI": "Fins", + "languages.ro-RO": "Roemeens", + "languages.sk-SK": "Slowaaks", "locations": { "child_locations": "Kind Locaties", "collapse_tree": "Structuur invouwen", diff --git a/frontend/locales/sl.json b/frontend/locales/sl.json index 43650036..689329e5 100644 --- a/frontend/locales/sl.json +++ b/frontend/locales/sl.json @@ -225,6 +225,10 @@ "zh-MO": "Kitajščina (Macau)", "zh-TW": "Kitajsščina (tradicionalna)" }, + "languages.da-DK": "Danščina", + "languages.fi.FI": "Finščina", + "languages.ro-RO": "Romunščina", + "languages.sk-SK": "Slovaščina", "locations": { "child_locations": "Podrejene lokacije", "collapse_tree": "Strni drevo", diff --git a/frontend/locales/zh-CN.json b/frontend/locales/zh-CN.json index f89aacf8..5681cb8d 100644 --- a/frontend/locales/zh-CN.json +++ b/frontend/locales/zh-CN.json @@ -9,6 +9,29 @@ } }, "global": { + "date_time": { + "ago": "{0} 之前", + "days": "天", + "hour": "小时", + "hours": "小时", + "just-now": "现在", + "last-month": "上个月", + "last-week": "上周", + "last-year": "去年", + "minute": "分钟", + "minutes": "分钟", + "months": "个月", + "next-month": "下个月", + "next-week": "下周", + "next-year": "明年", + "second": "秒", + "seconds": "秒", + "tomorrow": "明天", + "week": "周", + "weeks": "周", + "years": "年", + "yesterday": "昨天" + }, "page_qr_code": { "page_url": "页面URL" }, @@ -18,6 +41,8 @@ }, "item": { "create_modal": { + "item_description": "物品描述", + "item_name": "物品名称", "photo_button": "照片 📷", "title": "创建物品" }, @@ -27,29 +52,45 @@ "items": "物品", "no_items": "没有可展示的物品", "table": "表格模式" + }, + "table": { + "page": "页", + "rows_per_page": "每页行数" } } }, "label": { "create_modal": { + "label_description": "标签说明", + "label_name": "标签名称", "title": "创建标签" } }, "location": { "create_modal": { + "location_description": "位置描述", + "location_name": "位置名称", "title": "创建位置" }, + "selector": { + "parent_location": "父位置" + }, "tree": { "no_locations": "没有可用的位置。请添加新的位置\n `<`span class=\"link-primary\"`>`创建`<`/span`>` 按钮在导航栏上" } } }, "global": { + "add": "添加", "build": "编译:{build}", "confirm": "确认", "create": "创建", "create_and_add": "保存并继续创建", "created": "已创建", + "delete": "刪除", + "details": "详情", + "duplicate": "复制", + "edit": "修改", "email": "邮箱", "follow_dev": "关注开发者", "github": "Github项目", @@ -57,15 +98,29 @@ "join_discord": "加入Discord讨论", "labels": "标签", "locations": "位置", + "maintenance": "维护", "name": "名称", "password": "密码", "read_docs": "查阅文档", + "save": "保存", "search": "搜索", "sign_out": "登出", "submit": "提交", + "update": "更新", + "value": "值", "version": "版本:{version}", "welcome": "欢迎,{username}" }, + "home": { + "labels": "标签", + "quick_statistics": "快速统计", + "recently_added": "最近加入", + "storage_locations": "存放位置", + "total_items": "项目总量", + "total_labels": "标签总量", + "total_locations": "位置数量", + "total_value": "价值总和" + }, "index": { "disabled_registration": "已禁用注册", "dont_join_group": "不想加入群组?", @@ -80,32 +135,67 @@ }, "items": { "add": "添加", + "advanced": "进阶", + "archived": "已归档", + "asset_id": "资产ID", + "attachment": "附件", + "attachments": "附件", + "changes_persisted_immediately": "对附件的更改将会立即保存", "created_at": "创建于", "custom_fields": "自定义字段", + "description": "说明", + "details": "详情", + "drag_and_drop": "将文件拖拽到此或点击添加文件", + "edit_details": "编辑详情", "field_selector": "字段选择", "field_value": "字段值", "first": "第一页", "include_archive": "包括已存档的项目", + "insured": "保险", "last": "最后一页", + "lifetime_warranty": "包含终身保修", + "location": "位置", + "manufacturer": "制造商", + "model_number": "型号", + "name": "名称", "negate_labels": "取消选中的标签", "next_page": "下一页", "no_results": "没有可显示的物品", + "notes": "笔记", "options": "选项", "order_by": "排序方式", "pages": "第{page}页,共{totalPages}页", + "parent_item": "父项目", + "photo": "照片", + "photos": "照片", "prev_page": "上一页", + "purchase_date": "购买日期", + "purchase_details": "购买详情", + "purchase_price": "购买价格", + "purchased_from": "购买地址", + "quantity": "数量", "query_id": "查询资产ID: {id}", "reset_search": "重置搜索", "results": "{ total } 条结果", + "serial_number": "序列号", + "show_advanced_view_options": "显示高级选项", + "sold_at": "出售日期", + "sold_details": "售出详情", + "sold_price": "出售价格", + "sold_to": "出售对象", "tip_1": "位置和标签过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。", "tip_2": "以“#”为前缀的搜索将变成查询资产ID(例如“#000-001” )", "tip_3": "字段过滤器使用“或”操作。如果选择了多个位置或标签,\n则只需要任意一个匹配上即可。", "tips": "建议", "tips_sub": "搜索提示", - "updated_at": "更新于" + "updated_at": "更新于", + "warranty": "保修", + "warranty_details": "保修详情", + "warranty_expires": "保修持续到" }, "labels": { - "no_results": "未找到标签。" + "no_results": "未找到标签", + "update_label": "更新标签" }, "languages": { "ca": "加泰罗尼亚语", @@ -115,20 +205,30 @@ "fr": "法国", "hu": "匈牙利语", "it": "意大利语", + "ja-JP": "日本語", "nl": "荷兰语", "pl": "波兰语", "pt-BR": "葡萄牙语(巴西)", + "pt-PT": "葡萄牙语(葡萄牙)", "ru": "俄语", "sl": "斯洛文尼亚语", "sv": "瑞典语", "tr": "土耳其语", + "uk-UA": "乌克兰语", "zh-CN": "中文(简体)", "zh-HK": "中文(香港)", "zh-MO": "中文(澳门)", "zh-TW": "中文(繁体)" }, + "languages.da-DK": "丹麦语", + "languages.fi.FI": "芬兰语", + "languages.ro-RO": "罗马尼亚语", + "languages.sk-SK": "斯洛伐克语", "locations": { - "no_results": "找不到位置" + "child_locations": "子位置", + "collapse_tree": "折叠树", + "no_results": "找不到位置", + "update_location": "更新位置" }, "maintenance": { "filter": { @@ -166,6 +266,9 @@ "total_entries": "总项目" }, "menu": { + "create_item": "物品/资产", + "create_label": "标签", + "create_location": "位置", "home": "主页", "locations": "位置", "maintenance": "维护", @@ -182,6 +285,7 @@ "delete_account_sub": "删除您的帐户及其所有相关数据。这是无法撤消的。", "display_header": "{currentValue, select, true {隐藏页眉} false {显示页眉} other {未选中}}", "enabled": "已启用", + "example": "例子", "gen_invite": "生成邀请链接", "group_settings": "组设置", "group_settings_sub": "共享组设置。您可能需要刷新浏览器来让某些设置生效。", From 79ec169a5f3d156a1efae15c14fc6e46315177a8 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Wed, 30 Oct 2024 14:34:36 -0400 Subject: [PATCH 08/15] [VNEXT] feat: Multi-DB type support (#291) * feat: Multi-DB type URL formats and config * fix: remove legacy sqlite path config and minor other things * fix: dumb eslint issues * fix: dumb eslint issues * fix: application can be tested with sqlite * fix: minor config formatting * chore: some cleanup * feat: postgres migration creation now works The migration creation for postgres now works properly. Removed MySQL support, having too many issues with it at this time. * chore: revert some strings back to bytes as they should be * feat: improve languages support * feat: add locale time ago formatting and the local name for the language in language dropdown * Update FUNDING.yml * chore: remove some more mysql stuff * fix: coderabbit security recommendations * fix: validate postgres sslmode * Update migrations.go * fix: postgres migration creation now works * fix: errors in raw sql queries * fix: lint error, and simpler SQL query * fix: migrations directory string * fix: stats related test * fix: sql query * Update TextArea.vue * Update TextField.vue * chore: run integration testing on multiple postgresql versions * chore: jobs should run for vnext branch PRs * fix: missed $ for Postgres testing * fix: environment variable for db ssl mode * fix: lint issue from a merge * chore: trying to fix postgresql testing * chore: trying to fix postgresql testing * fix: trying to fix postgresql testing * fix: trying to fix postgresql testing --------- Co-authored-by: tonya --- .github/workflows/docker-publish-arm.yaml | 6 +- .../docker-publish-rootless-arm.yaml | 6 +- .../workflows/docker-publish-rootless.yaml | 6 +- .github/workflows/docker-publish.yaml | 6 +- .github/workflows/partial-frontend.yaml | 64 +++++++++++++++++ .github/workflows/pull-requests.yaml | 2 + .gitignore | 3 + Taskfile.yml | 54 ++++++++++++++- backend/app/api/main.go | 67 ++++++++++++++---- backend/app/tools/migrations/main.go | 69 ++++++++++++++++++- backend/go.mod | 1 + backend/go.sum | 44 ++++-------- .../internal/data/migrations/migrations.go | 14 ++-- .../postgres/20241027025146_init.sql | 58 ++++++++++++++++ .../data/migrations/postgres/atlas.sum | 2 + .../20220929052825_init.sql | 0 .../20221001210956_group_invitations.sql | 0 .../20221009173029_add_user_roles.sql | 0 .../20221020043305_allow_nesting_types.sql | 0 .../20221101041931_add_archived_field.sql | 0 .../20221113012312_add_asset_id_field.sql | 0 .../20221203053132_add_token_roles.sql | 0 .../20221205230404_drop_document_tokens.sql | 0 ...20221205234214_add_maintenance_entries.sql | 0 .../20221205234812_cascade_delete_roles.sql | 0 .../20230227024134_add_scheduled_date.sql | 0 .../20230305065819_add_notifier_types.sql | 0 ...230305071524_add_group_id_to_notifiers.sql | 0 ...1006213457_add_primary_attachment_flag.sql | 0 .../{migrations => sqlite3}/atlas.sum | 0 backend/internal/data/repo/repo_group.go | 45 ++++++------ backend/internal/data/repo/repo_items.go | 4 +- backend/internal/data/repo/repo_locations.go | 10 ++- backend/internal/sys/config/conf.go | 17 ++--- backend/internal/sys/config/conf_database.go | 14 +++- frontend/.eslintrc.js | 2 + 36 files changed, 387 insertions(+), 107 deletions(-) create mode 100644 backend/internal/data/migrations/postgres/20241027025146_init.sql create mode 100644 backend/internal/data/migrations/postgres/atlas.sum rename backend/internal/data/migrations/{migrations => sqlite3}/20220929052825_init.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221001210956_group_invitations.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221009173029_add_user_roles.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221020043305_allow_nesting_types.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221101041931_add_archived_field.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221113012312_add_asset_id_field.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221203053132_add_token_roles.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221205230404_drop_document_tokens.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221205234214_add_maintenance_entries.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20221205234812_cascade_delete_roles.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20230227024134_add_scheduled_date.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20230305065819_add_notifier_types.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20230305071524_add_group_id_to_notifiers.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/20231006213457_add_primary_attachment_flag.sql (100%) rename backend/internal/data/migrations/{migrations => sqlite3}/atlas.sum (100%) diff --git a/.github/workflows/docker-publish-arm.yaml b/.github/workflows/docker-publish-arm.yaml index 0147fa1c..56d93b45 100644 --- a/.github/workflows/docker-publish-arm.yaml +++ b/.github/workflows/docker-publish-arm.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless-arm.yaml b/.github/workflows/docker-publish-rootless-arm.yaml index c40cd55b..2ab20270 100644 --- a/.github/workflows/docker-publish-rootless-arm.yaml +++ b/.github/workflows/docker-publish-rootless-arm.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless.yaml b/.github/workflows/docker-publish-rootless.yaml index f5d022c6..82cd4153 100644 --- a/.github/workflows/docker-publish-rootless.yaml +++ b/.github/workflows/docker-publish-rootless.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index ab226bdb..2eff34b5 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -11,18 +11,18 @@ on: - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/partial-frontend.yaml b/.github/workflows/partial-frontend.yaml index c793c62a..0786b9eb 100644 --- a/.github/workflows/partial-frontend.yaml +++ b/.github/workflows/partial-frontend.yaml @@ -32,6 +32,20 @@ jobs: integration-tests: name: Integration Tests runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout uses: actions/checkout@v4 @@ -62,3 +76,53 @@ jobs: - name: Run Integration Tests run: task test:ci + integration-tests-pgsql: + strategy: + matrix: + database_version: [17,16,15] + name: Integration Tests PGSQL ${{ matrix.database_version }} + runs-on: ubuntu-latest + services: + postgres: + image: postgres:${{ matrix.database_version }} + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Task + uses: arduino/setup-task@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v3.0.0 + with: + version: 9.12.2 + + - name: Install dependencies + run: pnpm install + working-directory: frontend + + - name: Run Integration Tests + run: task test:ci:postgresql \ No newline at end of file diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml index 61a14859..e94977ac 100644 --- a/.github/workflows/pull-requests.yaml +++ b/.github/workflows/pull-requests.yaml @@ -4,10 +4,12 @@ on: pull_request: branches: - main + - vnext paths: - 'backend/**' - 'frontend/**' + - '.github/workflows/**' jobs: backend-tests: diff --git a/.gitignore b/.gitignore index 51a835a4..0159273c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ backend/.data/* config.yml homebox.db +homebox.db-journal +homebox.db-shm +homebox.db-wal .idea .DS_Store test-mailer.json diff --git a/Taskfile.yml b/Taskfile.yml index 4d9c1aa2..c945f260 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,7 +2,8 @@ version: "3" env: HBOX_LOG_LEVEL: debug - HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 + HBOX_DATABASE_DRIVER: sqlite3 + HBOX_DATABASE_SQLITE_PATH: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 HBOX_OPTIONS_ALLOW_REGISTRATION: true UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure" tasks: @@ -60,6 +61,23 @@ tasks: - go run ./app/api/ {{ .CLI_ARGS }} silent: false + go:run:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Starts the backend api server with postgresql (depends on generate task) + dir: backend + deps: + - generate + cmds: + - go run ./app/api/ {{ .CLI_ARGS }} + silent: false + go:test: desc: Runs all go tests using gotestsum - supports passing gotestsum args dir: backend @@ -114,6 +132,21 @@ tasks: cmds: - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + db:migration:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs the database diff engine to generate a SQL migration files for postgresql + deps: + - db:generate + cmds: + - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + ui:watch: desc: Starts the vitest test runner in watch mode dir: frontend @@ -143,7 +176,24 @@ tasks: cmds: - cd backend && go build ./app/api - backend/api & - - sleep 5 + - sleep 10 + - cd frontend && pnpm run test:ci + silent: true + + test:ci:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: 127.0.0.1 + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs end-to-end test on a live server with postgresql (only for use in CI) + cmds: + - cd backend && go build ./app/api + - backend/api & + - sleep 10 - cd frontend && pnpm run test:ci silent: true diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 6247fe6a..86efc5dd 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "fmt" + "github.com/google/uuid" "net/http" "os" "path/filepath" + "strings" "time" atlas "ariga.io/atlas/sql/migrate" @@ -28,6 +30,7 @@ import ( "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "github.com/sysadminsmedia/homebox/backend/internal/web/mid" + _ "github.com/lib/pq" _ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite" ) @@ -46,6 +49,19 @@ func build() string { return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) } +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} + // @title Homebox API // @version 1.0 // @description Track, Manage, and Organize your Things. @@ -80,13 +96,32 @@ func run(cfg *config.Config) error { log.Fatal().Err(err).Msg("failed to create data directory") } - c, err := ent.Open("sqlite3", cfg.Storage.SqliteURL) + if strings.ToLower(cfg.Database.Driver) == "postgres" { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode") + } + } + + // Set up the database URL based on the driver because for some reason a common URL format is not used + databaseURL := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + databaseURL = cfg.Database.SqlitePath + case "postgres": + databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver") + } + + c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL) if err != nil { log.Fatal(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). - Msg("failed opening connection to sqlite") + Str("driver", strings.ToLower(cfg.Database.Driver)). + Str("host", cfg.Database.Host). + Str("port", cfg.Database.Port). + Str("database", cfg.Database.Database). + Msg("failed opening connection to {driver} database at {host}:{port}/{database}") } defer func(c *ent.Client) { err := c.Close() @@ -95,9 +130,14 @@ func run(cfg *config.Config) error { } }(c) - temp := filepath.Join(os.TempDir(), "migrations") + // Always create a random temporary directory for migrations + tempUUID, err := uuid.NewUUID() + if err != nil { + return err + } + temp := filepath.Join(os.TempDir(), fmt.Sprintf("homebox-%s", tempUUID.String())) - err = migrations.Write(temp) + err = migrations.Write(temp, cfg.Database.Driver) if err != nil { return err } @@ -117,17 +157,18 @@ func run(cfg *config.Config) error { if err != nil { log.Error(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). + Str("driver", cfg.Database.Driver). + Str("url", databaseURL). Msg("failed creating schema resources") return err } - err = os.RemoveAll(temp) - if err != nil { - log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") - return err - } + defer func() { + err := os.RemoveAll(temp) + if err != nil { + log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") + } + }() collectFuncs := []currencies.CollectorFunc{ currencies.CollectDefaults(), diff --git a/backend/app/tools/migrations/main.go b/backend/app/tools/migrations/main.go index f0bd2c6d..fd952cc8 100644 --- a/backend/app/tools/migrations/main.go +++ b/backend/app/tools/migrations/main.go @@ -3,8 +3,11 @@ package main import ( "context" "fmt" + "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "log" "os" + "path/filepath" + "strings" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/migrate" @@ -12,13 +15,29 @@ import ( _ "ariga.io/atlas/sql/sqlite" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql/schema" + + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) func main() { + cfg, err := config.New(build(), "Homebox inventory management system") + if err != nil { + panic(err) + } + sqlDialect := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + sqlDialect = dialect.SQLite + case "postgres": + sqlDialect = dialect.Postgres + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } ctx := context.Background() // Create a local migration directory able to understand Atlas migration file format for replay. - dir, err := atlas.NewLocalDir("internal/data/migrations/migrations") + safePath := filepath.Clean(fmt.Sprintf("internal/data/migrations/%s", sqlDialect)) + dir, err := atlas.NewLocalDir(safePath) if err != nil { log.Fatalf("failed creating atlas migration directory: %v", err) } @@ -26,7 +45,7 @@ func main() { opts := []schema.MigrateOption{ schema.WithDir(dir), // provide migration directory schema.WithMigrationMode(schema.ModeReplay), // provide migration mode - schema.WithDialect(dialect.SQLite), // Ent dialect to use + schema.WithDialect(sqlDialect), // Ent dialect to use schema.WithFormatter(atlas.DefaultFormatter), schema.WithDropIndex(true), schema.WithDropColumn(true), @@ -35,11 +54,55 @@ func main() { log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go '") } + if sqlDialect == dialect.Postgres { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatalf("invalid sslmode: %s", cfg.Database.SslMode) + } + } + + databaseURL := "" + switch { + case cfg.Database.Driver == "sqlite3": + databaseURL = fmt.Sprintf("sqlite://%s", cfg.Database.SqlitePath) + case cfg.Database.Driver == "postgres": + databaseURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } + // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above). - err = migrate.NamedDiff(ctx, "sqlite://.data/homebox.migration.db?_fk=1", os.Args[1], opts...) + err = migrate.NamedDiff(ctx, databaseURL, os.Args[1], opts...) if err != nil { log.Fatalf("failed generating migration file: %v", err) } fmt.Println("Migration file generated successfully.") } + +var ( + version = "nightly" + commit = "HEAD" + buildTime = "now" +) + +func build() string { + short := commit + if len(short) > 7 { + short = short[:7] + } + + return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) +} + +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} diff --git a/backend/go.mod b/backend/go.mod index b2f2d597..fc7c524b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.4.1 github.com/hay-kot/httpkit v0.0.11 + github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.24 github.com/olahol/melody v1.2.1 github.com/pkg/errors v0.9.1 diff --git a/backend/go.sum b/backend/go.sum index f9a83abc..f210751e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,9 +1,5 @@ ariga.io/atlas v0.19.1 h1:QzBHkakwzEhmPWOzNhw8Yr/Bbicj6Iq5hwEoNI/Jr9A= ariga.io/atlas v0.19.1/go.mod h1:VPlcXdd4w2KqKnH54yEZcry79UAhpaWaxEsmn5JRNoE= -ariga.io/atlas v0.28.0 h1:qmn9tUyJypJkIw+X3ECUwDtkMTiFupgstHbjRN4xGH0= -ariga.io/atlas v0.28.0/go.mod h1:LOOp18LCL9r+VifvVlJqgYJwYl271rrXD9/wIyzJ8sw= -entgo.io/ent v0.12.5 h1:KREM5E4CSoej4zeGa88Ou/gfturAnpUv0mzAjch1sj4= -entgo.io/ent v0.12.5/go.mod h1:Y3JVAjtlIk8xVZYSn3t3mf8xlZIn5SAOXZQxD6kKI+Q= entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s= entgo.io/ent v0.14.1/go.mod h1:MH6XLG0KXpkcDQhKiHfANZSzR55TJyPL5IGNpI8wpco= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= @@ -58,8 +54,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -105,6 +99,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -116,10 +112,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= -github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -129,8 +121,6 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ= github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= @@ -148,10 +138,6 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -176,20 +162,12 @@ github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -197,16 +175,10 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= @@ -223,8 +195,14 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= @@ -233,8 +211,10 @@ modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= -modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM= modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= diff --git a/backend/internal/data/migrations/migrations.go b/backend/internal/data/migrations/migrations.go index a2afdc89..ae8852f4 100644 --- a/backend/internal/data/migrations/migrations.go +++ b/backend/internal/data/migrations/migrations.go @@ -3,23 +3,29 @@ package migrations import ( "embed" + "fmt" "os" "path" ) -//go:embed all:migrations +//go:embed all:sqlite3 all:postgres var Files embed.FS // Write writes the embedded migrations to a temporary directory. // It returns an error and a cleanup function. The cleanup function // should be called when the migrations are no longer needed. -func Write(temp string) error { +func Write(temp string, dialect string) error { + allowedDialects := map[string]bool{"sqlite3": true, "postgres": true} + if !allowedDialects[dialect] { + return fmt.Errorf("unsupported dialect: %s", dialect) + } + err := os.MkdirAll(temp, 0o755) if err != nil { return err } - fsDir, err := Files.ReadDir("migrations") + fsDir, err := Files.ReadDir(dialect) if err != nil { return err } @@ -29,7 +35,7 @@ func Write(temp string) error { continue } - b, err := Files.ReadFile(path.Join("migrations", f.Name())) + b, err := Files.ReadFile(path.Join(dialect, f.Name())) if err != nil { return err } diff --git a/backend/internal/data/migrations/postgres/20241027025146_init.sql b/backend/internal/data/migrations/postgres/20241027025146_init.sql new file mode 100644 index 00000000..a1ebcb70 --- /dev/null +++ b/backend/internal/data/migrations/postgres/20241027025146_init.sql @@ -0,0 +1,58 @@ +-- Create "groups" table +CREATE TABLE "groups" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "currency" character varying NOT NULL DEFAULT 'usd', PRIMARY KEY ("id")); +-- Create "documents" table +CREATE TABLE "documents" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "title" character varying NOT NULL, "path" character varying NOT NULL, "group_documents" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "documents_groups_documents" FOREIGN KEY ("group_documents") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "locations" table +CREATE TABLE "locations" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "group_locations" uuid NOT NULL, "location_children" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "locations_groups_locations" FOREIGN KEY ("group_locations") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "locations_locations_children" FOREIGN KEY ("location_children") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE SET NULL); +-- Create "items" table +CREATE TABLE "items" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "import_ref" character varying NULL, "notes" character varying NULL, "quantity" bigint NOT NULL DEFAULT 1, "insured" boolean NOT NULL DEFAULT false, "archived" boolean NOT NULL DEFAULT false, "asset_id" bigint NOT NULL DEFAULT 0, "serial_number" character varying NULL, "model_number" character varying NULL, "manufacturer" character varying NULL, "lifetime_warranty" boolean NOT NULL DEFAULT false, "warranty_expires" timestamptz NULL, "warranty_details" character varying NULL, "purchase_time" timestamptz NULL, "purchase_from" character varying NULL, "purchase_price" double precision NOT NULL DEFAULT 0, "sold_time" timestamptz NULL, "sold_to" character varying NULL, "sold_price" double precision NOT NULL DEFAULT 0, "sold_notes" character varying NULL, "group_items" uuid NOT NULL, "item_children" uuid NULL, "location_items" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "items_groups_items" FOREIGN KEY ("group_items") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "items_items_children" FOREIGN KEY ("item_children") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "items_locations_items" FOREIGN KEY ("location_items") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "item_archived" to table: "items" +CREATE INDEX "item_archived" ON "items" ("archived"); +-- Create index "item_asset_id" to table: "items" +CREATE INDEX "item_asset_id" ON "items" ("asset_id"); +-- Create index "item_manufacturer" to table: "items" +CREATE INDEX "item_manufacturer" ON "items" ("manufacturer"); +-- Create index "item_model_number" to table: "items" +CREATE INDEX "item_model_number" ON "items" ("model_number"); +-- Create index "item_name" to table: "items" +CREATE INDEX "item_name" ON "items" ("name"); +-- Create index "item_serial_number" to table: "items" +CREATE INDEX "item_serial_number" ON "items" ("serial_number"); +-- Create "attachments" table +CREATE TABLE "attachments" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "type" character varying NOT NULL DEFAULT 'attachment', "primary" boolean NOT NULL DEFAULT false, "document_attachments" uuid NOT NULL, "item_attachments" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "attachments_documents_attachments" FOREIGN KEY ("document_attachments") REFERENCES "documents" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "attachments_items_attachments" FOREIGN KEY ("item_attachments") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "users" table +CREATE TABLE "users" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_superuser" boolean NOT NULL DEFAULT false, "superuser" boolean NOT NULL DEFAULT false, "role" character varying NOT NULL DEFAULT 'user', "activated_on" timestamptz NULL, "group_users" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "users_groups_users" FOREIGN KEY ("group_users") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "users_email_key" to table: "users" +CREATE UNIQUE INDEX "users_email_key" ON "users" ("email"); +-- Create "auth_tokens" table +CREATE TABLE "auth_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "user_auth_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_tokens_users_auth_tokens" FOREIGN KEY ("user_auth_tokens") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "auth_tokens_token_key" to table: "auth_tokens" +CREATE UNIQUE INDEX "auth_tokens_token_key" ON "auth_tokens" ("token"); +-- Create index "authtokens_token" to table: "auth_tokens" +CREATE INDEX "authtokens_token" ON "auth_tokens" ("token"); +-- Create "auth_roles" table +CREATE TABLE "auth_roles" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "role" character varying NOT NULL DEFAULT 'user', "auth_tokens_roles" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_roles_auth_tokens_roles" FOREIGN KEY ("auth_tokens_roles") REFERENCES "auth_tokens" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "auth_roles_auth_tokens_roles_key" to table: "auth_roles" +CREATE UNIQUE INDEX "auth_roles_auth_tokens_roles_key" ON "auth_roles" ("auth_tokens_roles"); +-- Create "group_invitation_tokens" table +CREATE TABLE "group_invitation_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "uses" bigint NOT NULL DEFAULT 0, "group_invitation_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "group_invitation_tokens_groups_invitation_tokens" FOREIGN KEY ("group_invitation_tokens") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "group_invitation_tokens_token_key" to table: "group_invitation_tokens" +CREATE UNIQUE INDEX "group_invitation_tokens_token_key" ON "group_invitation_tokens" ("token"); +-- Create "item_fields" table +CREATE TABLE "item_fields" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "type" character varying NOT NULL, "text_value" character varying NULL, "number_value" bigint NULL, "boolean_value" boolean NOT NULL DEFAULT false, "time_value" timestamptz NOT NULL, "item_fields" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "item_fields_items_fields" FOREIGN KEY ("item_fields") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "labels" table +CREATE TABLE "labels" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "color" character varying NULL, "group_labels" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "labels_groups_labels" FOREIGN KEY ("group_labels") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "label_items" table +CREATE TABLE "label_items" ("label_id" uuid NOT NULL, "item_id" uuid NOT NULL, PRIMARY KEY ("label_id", "item_id"), CONSTRAINT "label_items_item_id" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "label_items_label_id" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "maintenance_entries" table +CREATE TABLE "maintenance_entries" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "date" timestamptz NULL, "scheduled_date" timestamptz NULL, "name" character varying NOT NULL, "description" character varying NULL, "cost" double precision NOT NULL DEFAULT 0, "item_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "maintenance_entries_items_maintenance_entries" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "notifiers" table +CREATE TABLE "notifiers" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "url" character varying NOT NULL, "is_active" boolean NOT NULL DEFAULT true, "group_id" uuid NOT NULL, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "notifiers_groups_notifiers" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "notifiers_users_notifiers" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "notifier_group_id" to table: "notifiers" +CREATE INDEX "notifier_group_id" ON "notifiers" ("group_id"); +-- Create index "notifier_group_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_group_id_is_active" ON "notifiers" ("group_id", "is_active"); +-- Create index "notifier_user_id" to table: "notifiers" +CREATE INDEX "notifier_user_id" ON "notifiers" ("user_id"); +-- Create index "notifier_user_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_user_id_is_active" ON "notifiers" ("user_id", "is_active"); diff --git a/backend/internal/data/migrations/postgres/atlas.sum b/backend/internal/data/migrations/postgres/atlas.sum new file mode 100644 index 00000000..c74d1c00 --- /dev/null +++ b/backend/internal/data/migrations/postgres/atlas.sum @@ -0,0 +1,2 @@ +h1:3BgMK0p+hw7ygZFPnd9W6s3IzPNXa87zYGty2LeNjEg= +20241027025146_init.sql h1:PJhm+pjGRtFfgmGu7MwJo8+bVelVfU5LB+LZ/c8nnGE= diff --git a/backend/internal/data/migrations/migrations/20220929052825_init.sql b/backend/internal/data/migrations/sqlite3/20220929052825_init.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20220929052825_init.sql rename to backend/internal/data/migrations/sqlite3/20220929052825_init.sql diff --git a/backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql b/backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql rename to backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql diff --git a/backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql b/backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql rename to backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql b/backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql rename to backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql diff --git a/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql b/backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql rename to backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql diff --git a/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql b/backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql rename to backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql diff --git a/backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql b/backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql rename to backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql b/backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql rename to backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql diff --git a/backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql b/backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql rename to backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql diff --git a/backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql b/backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql rename to backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql diff --git a/backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql b/backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql rename to backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql diff --git a/backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql b/backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql rename to backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql diff --git a/backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql b/backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql rename to backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql diff --git a/backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql b/backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql rename to backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql diff --git a/backend/internal/data/migrations/migrations/atlas.sum b/backend/internal/data/migrations/sqlite3/atlas.sum similarity index 100% rename from backend/internal/data/migrations/migrations/atlas.sum rename to backend/internal/data/migrations/sqlite3/atlas.sum diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 2498052e..74d5d171 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -161,16 +161,10 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, // Get the Totals for the Start and End of the Given Time Period q := ` SELECT - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_start, - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_end + SUM(CASE WHEN created_at < $1 THEN purchase_price ELSE 0 END) AS price_at_start, + SUM(CASE WHEN created_at < $2 THEN purchase_price ELSE 0 END) AS price_at_end + FROM items + WHERE group_items = $3 AND archived = false ` stats := ValueOverTime{ Start: start, @@ -180,7 +174,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, var maybeStart *float64 var maybeEnd *float64 - row := r.db.Sql().QueryRowContext(ctx, q, gid, sqliteDateFormat(start), gid, sqliteDateFormat(end)) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(start), sqliteDateFormat(end), gid) err := row.Scan(&maybeStart, &maybeEnd) if err != nil { return nil, err @@ -229,20 +223,25 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) { q := ` SELECT - (SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users, - (SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items, - (SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations, - (SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels, - (SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = ? AND items.archived = false) AS total_item_price, - (SELECT COUNT(*) - FROM items - WHERE group_items = ? - AND items.archived = false - AND (items.lifetime_warranty = true OR items.warranty_expires > date()) - ) AS total_with_warranty + COUNT(DISTINCT u.id) as total_users, + COUNT(DISTINCT CASE WHEN i.archived = false THEN i.id END) as total_items, + COUNT(DISTINCT l.id) as total_locations, + COUNT(DISTINCT lb.id) as total_labels, + SUM(CASE WHEN i.archived = false THEN i.purchase_price * i.quantity ELSE 0 END) as total_item_price, + COUNT(DISTINCT CASE + WHEN i.archived = false + AND (i.lifetime_warranty = true OR i.warranty_expires > $1) + THEN i.id + END) as total_with_warranty +FROM groups g + LEFT JOIN users u ON u.group_users = g.id + LEFT JOIN items i ON i.group_items = g.id + LEFT JOIN locations l ON l.group_locations = g.id + LEFT JOIN labels lb ON lb.group_labels = g.id +WHERE g.id = $2; ` var stats GroupStatistics - row := r.db.Sql().QueryRowContext(ctx, q, gid, gid, gid, gid, gid, gid) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(time.Now()), gid) var maybeTotalItemPrice *float64 var maybeTotalWithWarranty *int diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index cffc910f..830205a8 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -92,12 +92,12 @@ type ( // Purchase PurchaseTime types.Date `json:"purchaseTime"` - PurchaseFrom string `json:"purchaseFrom" validate:"max=255"` + PurchaseFrom string `json:"purchaseFrom" validate:"max=255"` PurchasePrice float64 `json:"purchasePrice" extensions:"x-nullable,x-omitempty"` // Sold SoldTime types.Date `json:"soldTime"` - SoldTo string `json:"soldTo" validate:"max=255"` + SoldTo string `json:"soldTo" validate:"max=255"` SoldPrice float64 `json:"soldPrice" extensions:"x-nullable,x-omitempty"` SoldNotes string `json:"soldNotes"` diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 7ece91b2..d820a166 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -121,7 +121,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, gid uuid.UUID, filter L FROM locations WHERE - locations.group_locations = ? {{ FILTER_CHILDREN }} + locations.group_locations = $1 {{ FILTER_CHILDREN }} ORDER BY locations.name ASC ` @@ -278,8 +278,8 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, gid, locID uuid.UUI query := `WITH RECURSIVE location_path AS ( SELECT id, name, location_children FROM locations - WHERE id = ? -- Replace ? with the ID of the item's location - AND group_locations = ? -- Replace ? with the ID of the group + WHERE id = $1 -- Replace ? with the ID of the item's location + AND group_locations = $2 -- Replace ? with the ID of the group UNION ALL @@ -332,7 +332,7 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue 'location' AS node_type FROM locations WHERE location_children IS NULL - AND group_locations = ? + AND group_locations = $1 UNION ALL SELECT c.id, @@ -355,10 +355,8 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue SELECT * FROM location_tree - {{ WITH_ITEMS_FROM }} - ) tree ORDER BY node_type DESC, -- sort locations before items level, diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 8b7b23c3..9b531416 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -18,14 +18,15 @@ const ( type Config struct { conf.Version - Mode string `yaml:"mode" conf:"default:development"` // development or production - Web WebConfig `yaml:"web"` - Storage Storage `yaml:"storage"` - Log LoggerConf `yaml:"logger"` - Mailer MailerConf `yaml:"mailer"` - Demo bool `yaml:"demo"` - Debug DebugConf `yaml:"debug"` - Options Options `yaml:"options"` + Mode string `yaml:"mode" conf:"default:development"` // development or production + Web WebConfig `yaml:"web"` + Storage Storage `yaml:"storage"` + Database Database `yaml:"database"` + Log LoggerConf `yaml:"logger"` + Mailer MailerConf `yaml:"mailer"` + Demo bool `yaml:"demo"` + Debug DebugConf `yaml:"debug"` + Options Options `yaml:"options"` } type Options struct { diff --git a/backend/internal/sys/config/conf_database.go b/backend/internal/sys/config/conf_database.go index 2c6a7610..06d44bfe 100644 --- a/backend/internal/sys/config/conf_database.go +++ b/backend/internal/sys/config/conf_database.go @@ -6,6 +6,16 @@ const ( type Storage struct { // Data is the path to the root directory - Data string `yaml:"data" conf:"default:./.data"` - SqliteURL string `yaml:"sqlite-url" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` + Data string `yaml:"data" conf:"default:./.data"` +} + +type Database struct { + Driver string `yaml:"driver" conf:"default:sqlite3"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Host string `yaml:"host"` + Port string `yaml:"port"` + Database string `yaml:"database"` + SslMode string `yaml:"ssl_mode"` + SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` } diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index e018b4b1..a865e8ea 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -26,8 +26,10 @@ module.exports = { "vue/no-setup-props-destructure": 0, "vue/no-multiple-template-root": 0, "vue/no-v-model-argument": 0, + "vue/no-v-html": 0, "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/ban-ts-comment": 0, + "tailwindcss/no-custom-classname": 0, "@typescript-eslint/no-unused-vars": [ "error", { From e65e31a067b26a6b7f3fd1c8afb781cd798f2da8 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Wed, 30 Oct 2024 14:38:53 -0400 Subject: [PATCH 09/15] fix: publish docker vnext branch --- .github/workflows/docker-publish-arm.yaml | 2 +- .github/workflows/docker-publish-rootless-arm.yaml | 2 +- .github/workflows/docker-publish-rootless.yaml | 2 +- .github/workflows/docker-publish.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-publish-arm.yaml b/.github/workflows/docker-publish-arm.yaml index 56d93b45..fa8de098 100644 --- a/.github/workflows/docker-publish-arm.yaml +++ b/.github/workflows/docker-publish-arm.yaml @@ -4,7 +4,7 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' diff --git a/.github/workflows/docker-publish-rootless-arm.yaml b/.github/workflows/docker-publish-rootless-arm.yaml index 2ab20270..2fee317e 100644 --- a/.github/workflows/docker-publish-rootless-arm.yaml +++ b/.github/workflows/docker-publish-rootless-arm.yaml @@ -4,7 +4,7 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' diff --git a/.github/workflows/docker-publish-rootless.yaml b/.github/workflows/docker-publish-rootless.yaml index 82cd4153..8dd77519 100644 --- a/.github/workflows/docker-publish-rootless.yaml +++ b/.github/workflows/docker-publish-rootless.yaml @@ -4,7 +4,7 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index 2eff34b5..6b1b63e9 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -4,7 +4,7 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' From 5ad79d2fbd3f23598d9b438635c69ad67c4cf0a8 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Wed, 30 Oct 2024 14:51:03 -0400 Subject: [PATCH 10/15] Add upgrade guide documentation --- docs/.vitepress/config.mts | 6 ++++++ docs/.vitepress/menus/en.mts | 3 ++- docs/en/upgrade.md | 13 +++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 docs/en/upgrade.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 263b1ceb..0e14fa55 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -54,5 +54,11 @@ export default defineConfig({ { icon: 'github', link: 'https://git.homebox.software' }, { icon: 'mastodon', link: 'https://noc.social/@sysadminszone' }, ] + }, + + markdown: { + image: { + lazyLoading: true + } } }) diff --git a/docs/.vitepress/menus/en.mts b/docs/.vitepress/menus/en.mts index 9063d773..74ad85e2 100644 --- a/docs/.vitepress/menus/en.mts +++ b/docs/.vitepress/menus/en.mts @@ -4,8 +4,9 @@ export default [ items: [ {text: 'Quick Start', link: '/en/quick-start'}, {text: 'Installation', link: '/en/installation'}, - {text: 'Organizing Your Items', link: '/en/organizing-items'}, {text: 'Configure Homebox', link: '/en/configure-homebox'}, + {text: 'Upgrade', link: '/en/upgrade'}, + {text: 'Organizing Your Items', link: '/en/organizing-items'}, {text: 'Tips and Tricks', link: '/en/tips-tricks'} ] }, diff --git a/docs/en/upgrade.md b/docs/en/upgrade.md new file mode 100644 index 00000000..2b7f34d3 --- /dev/null +++ b/docs/en/upgrade.md @@ -0,0 +1,13 @@ +# Upgrade + +## From v0.x.x to v1.0.0 + +::: danger Breaking Changes +This upgrade process involves some potentially breaking changes, please review this documentation carefully before beginning the upgrade process, and follow it closely during your upgrade. +::: + +### Configuration Changes +#### Database Configuration +- `HBOX_STORAGE_SQLITE_URL` has been replaced by `HBOX_DATABASE_SQLITE_PATH` +- `HBOX_DATABASE_DRIVER` has been added to set the database type, valid options are `sqlite3` and `postgres` +- `HBOX_DATABASE_HOST`, `HBOX_DATABASE_PORT`, `HBOX_DATABASE_USERNAME`, `HBOX_DATABASE_DATABASE`, and `HBOX_DATABASE_SSL_MODE` have been added to configure postgres connection options. \ No newline at end of file From 16a6c3520a930103d94140b37159d263af9e46e7 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Thu, 31 Oct 2024 19:33:37 -0400 Subject: [PATCH 11/15] chore: add new config options to documentation --- docs/en/configure-homebox.md | 55 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/en/configure-homebox.md b/docs/en/configure-homebox.md index 138193e8..565b792f 100644 --- a/docs/en/configure-homebox.md +++ b/docs/en/configure-homebox.md @@ -2,29 +2,36 @@ ## Env Variables & Configuration -| Variable | Default | Description | -| ------------------------------------ | ---------------------- | ---------------------------------------------------------------------------------- | -| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` | -| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this | -| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this | -| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves | -| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items | -| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie | -| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB | -| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever | -| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server | -| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server | -| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker | -| HBOX_STORAGE_SQLITE_URL | /data/homebox.db?_fk=1 | sqlite database url, if you're using docker do not change this | -| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` | -| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` | -| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used | -| HBOX_MAILER_PORT | 587 | email port to use | -| HBOX_MAILER_USERNAME | | email user to use | -| HBOX_MAILER_PASSWORD | | email password to use | -| HBOX_MAILER_FROM | | email from address to use | -| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled | -| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` | +| Variable | Default | Description | +|--------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| HBOX_MODE | `production` | application mode used for runtime behavior can be one of: `development`, `production` | +| HBOX_WEB_PORT | 7745 | port to run the web server on, if you're using docker do not change this | +| HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this | +| HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves | +| HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items | +| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie | +| HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB | +| HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever | +| HBOX_WEB_WRITE_TIMEOUT | 10s | Write timeout of HTTP server | +| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server | +| HBOX_STORAGE_DATA | /data/ | path to the data directory, do not change this if you're using docker | +| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` | +| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` | +| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used | +| HBOX_MAILER_PORT | 587 | email port to use | +| HBOX_MAILER_USERNAME | | email user to use | +| HBOX_MAILER_PASSWORD | | email password to use | +| HBOX_MAILER_FROM | | email from address to use | +| HBOX_SWAGGER_HOST | 7745 | swagger host to use, if not set swagger will be disabled | +| HBOX_SWAGGER_SCHEMA | `http` | swagger schema to use, can be one of: `http`, `https` | +| HBOX_DATABASE_TYPE | sqlite3 | sets the correct database type (`sqlite3` or `postgres`) | +| HBOX_DATABASE_SQLITE_PATH | ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1 | sets the directory path for Sqlite | +| HBOX_DATABASE_HOST | | sets the hostname for a postgres database | +| HBOX_DATABASE_PORT | | sets the port for a postgres database | +| HBOX_DATABASE_USERNAME | | sets the username for a postgres connection | +| HBOX_DATABASE_PASSWORD | | sets the password for a postgres connection | +| HBOX_DATABASE_DATABASE | | sets the database for a postgres connection | +| HBOX_DATABASE_SSL_MODE | | sets the sslmode for a postgres connection | ::: tip "CLI Arguments" If you're deploying without docker you can use command line arguments to configure the application. Run `homebox --help` for more information. @@ -38,7 +45,7 @@ OPTIONS --web-host/$HBOX_WEB_HOST --web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE (default: 10) --storage-data/$HBOX_STORAGE_DATA (default: ./.data) ---storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL (default: ./.data/homebox.db?_fk=1) +--storage-sqlite-url/$HBOX_STORAGE_SQLITE_URL (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1) --log-level/$HBOX_LOG_LEVEL (default: info) --log-format/$HBOX_LOG_FORMAT (default: text) --mailer-host/$HBOX_MAILER_HOST From 0c7e60630a71e631539bbfed7e0d8ab4d2160449 Mon Sep 17 00:00:00 2001 From: Katos <7927609+katosdev@users.noreply.github.com> Date: Fri, 1 Nov 2024 17:30:31 +0000 Subject: [PATCH 12/15] Update Dockerfile Update dockerfile to test the theory of data folder breaking in vnext --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 878a1231..31de45fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,8 +66,10 @@ ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=j # Install necessary runtime dependencies RUN apk --no-cache add ca-certificates wget -# Create application directory and copy over built Go binary -RUN mkdir /app +# Create application and data directories with the correct permissions +RUN mkdir -p /app /data && chmod -R 777 /data + +# Copy over built Go binary COPY --from=builder /go/bin/api /app RUN chmod +x /app/api From 869e38b03db66ce1f1ff86c29ca15dccad7e8b17 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Fri, 1 Nov 2024 13:48:20 -0400 Subject: [PATCH 13/15] fix: broken docker image --- Dockerfile | 2 +- Dockerfile.rootless | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 31de45fa..f9a44ba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM --platform=$TARGETPLATFORM alpine:latest ENV HBOX_MODE=production ENV HBOX_STORAGE_DATA=/data/ -ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1 +ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_pragma=busy_timeout=2000&_pragma=journal_mode=WAL&_fk=1 # Install necessary runtime dependencies RUN apk --no-cache add ca-certificates wget diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 91ac930e..cc9d84fe 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -43,7 +43,7 @@ FROM gcr.io/distroless/static:latest ENV HBOX_MODE=production ENV HBOX_STORAGE_DATA=/data/ -ENV HBOX_STORAGE_SQLITE_URL=/data/homebox.db?_fk=1 +ENV HBOX_DATABASE_SQLITE_PATH=/data/homebox.db?_fk=1 # Copy the binary and data directory, change ownership COPY --from=builder --chown=nonroot /go/bin/api /app From bc99fd5b1d3d7872e1f85a4aa4b6ab201064807f Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Fri, 1 Nov 2024 22:06:58 -0400 Subject: [PATCH 14/15] fix: statistics --- backend/internal/data/repo/repo_group.go | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 74d5d171..de1f01f4 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -223,22 +223,17 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) { q := ` SELECT - COUNT(DISTINCT u.id) as total_users, - COUNT(DISTINCT CASE WHEN i.archived = false THEN i.id END) as total_items, - COUNT(DISTINCT l.id) as total_locations, - COUNT(DISTINCT lb.id) as total_labels, - SUM(CASE WHEN i.archived = false THEN i.purchase_price * i.quantity ELSE 0 END) as total_item_price, - COUNT(DISTINCT CASE - WHEN i.archived = false - AND (i.lifetime_warranty = true OR i.warranty_expires > $1) - THEN i.id - END) as total_with_warranty -FROM groups g - LEFT JOIN users u ON u.group_users = g.id - LEFT JOIN items i ON i.group_items = g.id - LEFT JOIN locations l ON l.group_locations = g.id - LEFT JOIN labels lb ON lb.group_labels = g.id -WHERE g.id = $2; + (SELECT COUNT(*) FROM users WHERE group_users = $2) AS total_users, + (SELECT COUNT(*) FROM items WHERE group_items = $2 AND items.archived = false) AS total_items, + (SELECT COUNT(*) FROM locations WHERE group_locations = $2) AS total_locations, + (SELECT COUNT(*) FROM labels WHERE group_labels = $2) AS total_labels, + (SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = $2 AND items.archived = false) AS total_item_price, + (SELECT COUNT(*) + FROM items + WHERE group_items = $2 + AND items.archived = false + AND (items.lifetime_warranty = true OR items.warranty_expires > $1) + ) AS total_with_warranty; ` var stats GroupStatistics row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(time.Now()), gid) From c00bce5351d364ce73ae402d7884c6107e6a7573 Mon Sep 17 00:00:00 2001 From: Matt Kilgore Date: Sat, 2 Nov 2024 15:38:29 -0400 Subject: [PATCH 15/15] feat: support mm, cm and inches for label generation --- Taskfile.yml | 17 ++++---- frontend/pages/reports/label-generator.vue | 48 ++++++++++++---------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index c945f260..35558d12 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -53,6 +53,8 @@ tasks: - cp ./backend/app/api/static/docs/swagger.json docs/docs/api/openapi-2.0.json go:run: + env: + HBOX_DEMO: true desc: Starts the backend api server (depends on generate task) dir: backend deps: @@ -63,13 +65,14 @@ tasks: go:run:postgresql: env: - HBOX_DATABASE_DRIVER: postgres - HBOX_DATABASE_USERNAME: homebox - HBOX_DATABASE_PASSWORD: homebox - HBOX_DATABASE_DATABASE: homebox - HBOX_DATABASE_HOST: localhost - HBOX_DATABASE_PORT: 5432 - HBOX_DATABASE_SSL_MODE: disable + HBOX_DEMO: true + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable desc: Starts the backend api server with postgresql (depends on generate task) dir: backend deps: diff --git a/frontend/pages/reports/label-generator.vue b/frontend/pages/reports/label-generator.vue index 2ce40903..5363955d 100644 --- a/frontend/pages/reports/label-generator.vue +++ b/frontend/pages/reports/label-generator.vue @@ -17,6 +17,7 @@ baseURL: window.location.origin, assetRange: 1, assetRangeMax: 91, + measure: "in", gapY: 0.25, columns: 3, cardHeight: 1, @@ -30,6 +31,7 @@ }); type Input = { + measure: string; page: { height: number; width: number; @@ -43,6 +45,7 @@ }; type Output = { + measure: string; cols: number; rows: number; gapY: number; @@ -66,6 +69,9 @@ function calculateGridData(input: Input): Output { const { page, cardHeight, cardWidth } = input; + const measureRegex = /in|cm|mm/; + const measure = measureRegex.test(input.measure) ? input.measure : "in"; + const availablePageWidth = page.width - page.pageLeftPadding - page.pageRightPadding; const availablePageHeight = page.height - page.pageTopPadding - page.pageBottomPadding; @@ -80,6 +86,7 @@ const gapY = (page.height - rows * cardHeight) / (rows - 1); return { + measure, cols, rows, gapX, @@ -115,6 +122,11 @@ label: "Asset End", ref: "assetRangeMax", }, + { + label: "Measure Type", + ref: "measure", + type: "text", + }, { label: "Label Height", ref: "cardHeight", @@ -242,6 +254,7 @@ const pages = ref([]); const out = ref({ + measure: "in", cols: 0, rows: 0, gapY: 0, @@ -263,6 +276,7 @@ function calcPages() { // Set Out Dimensions out.value = calculateGridData({ + measure: displayProperties.measure, page: { height: displayProperties.pageHeight, width: displayProperties.pageWidth, @@ -393,11 +407,11 @@ :key="pi" class="border-2 print:border-none" :style="{ - paddingTop: `${out.page.pt}in`, - paddingBottom: `${out.page.pb}in`, - paddingLeft: `${out.page.pl}in`, - paddingRight: `${out.page.pr}in`, - width: `${out.page.width}in`, + paddingTop: `${out.page.pt}${out.measure}`, + paddingBottom: `${out.page.pb}${out.measure}`, + paddingLeft: `${out.page.pl}${out.measure}`, + paddingRight: `${out.page.pr}${out.measure}`, + width: `${out.page.width}${out.measure}`, }" >
@@ -442,12 +456,4 @@
- - - + \ No newline at end of file