From b06cf0e5f431a99b77a664736c83d545149dcb72 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 14:57:26 +0100 Subject: [PATCH 1/8] feat: add JSON schema for project model with required properties and tags --- data/models/schemas/project.json | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 data/models/schemas/project.json diff --git a/data/models/schemas/project.json b/data/models/schemas/project.json new file mode 100644 index 00000000..f78462eb --- /dev/null +++ b/data/models/schemas/project.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["name", "repositories"], + "properties": { + "name": { + "type": "string", + "description": "The name of the project" + }, + "repositories": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["provider", "owner", "name"], + "properties": { + "provider": { + "type": "string", + "enum": ["github"], + "description": "The repository hosting provider" + }, + "owner": { + "type": "string", + "description": "The owner/organization of the repository" + }, + "name": { + "type": "string", + "description": "The name of the repository" + } + } + } + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "enum": ["by-algerian", "solve-algerian-problem"] + }, + "minItems": 1, + "description": "Tags associated with the project" + } + } +} From d8695ef4f72be9b57398a2dc1a1c8a97483e9bc2 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 14:57:37 +0100 Subject: [PATCH 2/8] feat: add tags and JSON schema reference to project info files --- data/models/projects/AiPalettes/info.json | 4 +++- data/models/projects/Algeria_Covid19_Tracker/info.json | 4 +++- data/models/projects/Algeria_Logos/info.json | 4 +++- data/models/projects/Algeria_Startup_Jobs/info.json | 4 +++- data/models/projects/Algeria_boukal/info.json | 4 +++- .../projects/Algerian_Administrative_Division/info.json | 4 +++- data/models/projects/Algerian_Education_System/info.json | 4 +++- data/models/projects/AlgeriansCoinsObjectDetection/info.json | 4 +++- data/models/projects/Archiy_Package/info.json | 4 +++- data/models/projects/Check_Hadith/info.json | 4 +++- data/models/projects/Courier_Dz/info.json | 4 +++- data/models/projects/Face_Mask_Detect/info.json | 4 +++- data/models/projects/Flutter_Reaction_Button/info.json | 4 +++- data/models/projects/Fun/info.json | 4 +++- data/models/projects/Godaddy_Reseller/info.json | 4 +++- data/models/projects/Gold_Prices_Web/info.json | 4 +++- data/models/projects/List_to_Tree_aka_l2t/info.json | 4 +++- data/models/projects/MERN_Auth_Roles_Boilerplate/info.json | 4 +++- data/models/projects/Melyon_Theme/info.json | 4 +++- data/models/projects/Mishkal/info.json | 4 +++- data/models/projects/Moadaly/info.json | 4 +++- data/models/projects/Mylinks_Space/info.json | 4 +++- data/models/projects/Openadhan/info.json | 4 +++- data/models/projects/PHP_JSON_Tongue/info.json | 4 +++- data/models/projects/Python_Complex/info.json | 4 +++- data/models/projects/QuranIPFS/info.json | 4 +++- data/models/projects/React_Glassmorphism_Components/info.json | 4 +++- .../models/projects/React_Native_Currency_Converter/info.json | 4 +++- data/models/projects/React_Project_Builder/info.json | 4 +++- data/models/projects/STRM_Test/info.json | 4 +++- data/models/projects/System_Monitor/info.json | 4 +++- data/models/projects/Violence_Detection/info.json | 4 +++- data/models/projects/Voice_Translator_React_Native/info.json | 4 +++- data/models/projects/Weather_Forecast/info.json | 4 +++- data/models/projects/list.json | 2 +- data/models/projects/who_want_to_millionaire/info.json | 4 +++- 36 files changed, 106 insertions(+), 36 deletions(-) diff --git a/data/models/projects/AiPalettes/info.json b/data/models/projects/AiPalettes/info.json index c9fec86d..0eb7139d 100644 --- a/data/models/projects/AiPalettes/info.json +++ b/data/models/projects/AiPalettes/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "AiPalettes", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "bondbenz", "name": "aipalettes" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Algeria_Covid19_Tracker/info.json b/data/models/projects/Algeria_Covid19_Tracker/info.json index 751d926e..4bb87e85 100644 --- a/data/models/projects/Algeria_Covid19_Tracker/info.json +++ b/data/models/projects/Algeria_Covid19_Tracker/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "dz-covid19.com", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "DGLcsGaming", "name": "dz-covid19.com" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Algeria_Logos/info.json b/data/models/projects/Algeria_Logos/info.json index 754f75be..0acb89c2 100644 --- a/data/models/projects/Algeria_Logos/info.json +++ b/data/models/projects/Algeria_Logos/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algeria Logos", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "aimen08", "name": "algerialogos" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Algeria_Startup_Jobs/info.json b/data/models/projects/Algeria_Startup_Jobs/info.json index c7c67558..386b1e24 100644 --- a/data/models/projects/Algeria_Startup_Jobs/info.json +++ b/data/models/projects/Algeria_Startup_Jobs/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algeria Startup Jobs", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "algeriastartupjobs", "name": "algeriastartupjobs.com" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Algeria_boukal/info.json b/data/models/projects/Algeria_boukal/info.json index 79b9d4bf..4a99d36a 100644 --- a/data/models/projects/Algeria_boukal/info.json +++ b/data/models/projects/Algeria_boukal/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algeria boukal", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "jusinamine", "name": "algeria-boukal" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Algerian_Administrative_Division/info.json b/data/models/projects/Algerian_Administrative_Division/info.json index 2193cd0b..8443e175 100644 --- a/data/models/projects/Algerian_Administrative_Division/info.json +++ b/data/models/projects/Algerian_Administrative_Division/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algerian Administrative Division", "repositories": [ { @@ -36,5 +37,6 @@ "owner": "dfourcfive", "name": "dzair_data_usage" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Algerian_Education_System/info.json b/data/models/projects/Algerian_Education_System/info.json index 99008f88..1a739854 100644 --- a/data/models/projects/Algerian_Education_System/info.json +++ b/data/models/projects/Algerian_Education_System/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algerian education system", "repositories": [ { @@ -11,5 +12,6 @@ "owner": "dzcode-io", "name": "kuliya" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/AlgeriansCoinsObjectDetection/info.json b/data/models/projects/AlgeriansCoinsObjectDetection/info.json index 47581aac..750461dd 100644 --- a/data/models/projects/AlgeriansCoinsObjectDetection/info.json +++ b/data/models/projects/AlgeriansCoinsObjectDetection/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Algerians Coins Object Detection", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "lablack576", "name": "algerians_coins_object_detection" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Archiy_Package/info.json b/data/models/projects/Archiy_Package/info.json index 9e28dabd..56c4ebec 100644 --- a/data/models/projects/Archiy_Package/info.json +++ b/data/models/projects/Archiy_Package/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Archy Package", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "yassine-youcefi", "name": "Archy" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Check_Hadith/info.json b/data/models/projects/Check_Hadith/info.json index d57bae68..723d6d60 100644 --- a/data/models/projects/Check_Hadith/info.json +++ b/data/models/projects/Check_Hadith/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Check Hadith", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "adelpro", "name": "check-hadith" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Courier_Dz/info.json b/data/models/projects/Courier_Dz/info.json index 18a46767..bfac63a9 100644 --- a/data/models/projects/Courier_Dz/info.json +++ b/data/models/projects/Courier_Dz/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Courier DZ", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "PiteurStudio", "name": "CourierDZ" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Face_Mask_Detect/info.json b/data/models/projects/Face_Mask_Detect/info.json index 0da4cb6a..dd8eda8a 100644 --- a/data/models/projects/Face_Mask_Detect/info.json +++ b/data/models/projects/Face_Mask_Detect/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Facial Mask Detection", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "Ghali-Benbernou", "name": "mask-detect" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Flutter_Reaction_Button/info.json b/data/models/projects/Flutter_Reaction_Button/info.json index ce6fdbba..22a679f5 100644 --- a/data/models/projects/Flutter_Reaction_Button/info.json +++ b/data/models/projects/Flutter_Reaction_Button/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Flutter Reaction Button", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "geekabdelouahed", "name": "flutter-reaction-button" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Fun/info.json b/data/models/projects/Fun/info.json index 8c22fb08..20d9dca2 100644 --- a/data/models/projects/Fun/info.json +++ b/data/models/projects/Fun/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Fun programming language", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "omdxp", "name": "fun" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Godaddy_Reseller/info.json b/data/models/projects/Godaddy_Reseller/info.json index dd772451..b0bbbd41 100644 --- a/data/models/projects/Godaddy_Reseller/info.json +++ b/data/models/projects/Godaddy_Reseller/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Godaddy Reseller", "repositories": [ { @@ -11,5 +12,6 @@ "owner": "Omar-Belghaouti", "name": "godaddy-reseller-api-client" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Gold_Prices_Web/info.json b/data/models/projects/Gold_Prices_Web/info.json index 77f16897..00562ad4 100644 --- a/data/models/projects/Gold_Prices_Web/info.json +++ b/data/models/projects/Gold_Prices_Web/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Gold prices web", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "jusinamine", "name": "gold-prices-web" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/List_to_Tree_aka_l2t/info.json b/data/models/projects/List_to_Tree_aka_l2t/info.json index dc1148a6..0403cf08 100644 --- a/data/models/projects/List_to_Tree_aka_l2t/info.json +++ b/data/models/projects/List_to_Tree_aka_l2t/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "List to Tree", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "ZibanPirate", "name": "l2t" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/MERN_Auth_Roles_Boilerplate/info.json b/data/models/projects/MERN_Auth_Roles_Boilerplate/info.json index e134b698..6e4cba32 100644 --- a/data/models/projects/MERN_Auth_Roles_Boilerplate/info.json +++ b/data/models/projects/MERN_Auth_Roles_Boilerplate/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "MERN Auth Roles Boilerplate", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "adelpro", "name": "MERN-auth-roles-boilerplate" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Melyon_Theme/info.json b/data/models/projects/Melyon_Theme/info.json index 477537ed..09d6e1e0 100644 --- a/data/models/projects/Melyon_Theme/info.json +++ b/data/models/projects/Melyon_Theme/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Melyon", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "CA1R7", "name": "melyon" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Mishkal/info.json b/data/models/projects/Mishkal/info.json index 9bfa2b7a..eb3e3496 100644 --- a/data/models/projects/Mishkal/info.json +++ b/data/models/projects/Mishkal/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Mishkal", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "linuxscout", "name": "mishkal" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Moadaly/info.json b/data/models/projects/Moadaly/info.json index 17e07ac0..93c77428 100644 --- a/data/models/projects/Moadaly/info.json +++ b/data/models/projects/Moadaly/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Moadaly: Moyenne calculatrice", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "madjsmail", "name": "moadaly" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/Mylinks_Space/info.json b/data/models/projects/Mylinks_Space/info.json index 37214857..8fabb059 100644 --- a/data/models/projects/Mylinks_Space/info.json +++ b/data/models/projects/Mylinks_Space/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Mylinks", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "bacloud23", "name": "mylinks" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Openadhan/info.json b/data/models/projects/Openadhan/info.json index 20eeea55..79620857 100644 --- a/data/models/projects/Openadhan/info.json +++ b/data/models/projects/Openadhan/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Openadhan", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "adelpro", "name": "Openadhan" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/PHP_JSON_Tongue/info.json b/data/models/projects/PHP_JSON_Tongue/info.json index 02cfdde2..d586131d 100644 --- a/data/models/projects/PHP_JSON_Tongue/info.json +++ b/data/models/projects/PHP_JSON_Tongue/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "PHP JSON Tongue", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "elaborate-code", "name": "php-json-tongue" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Python_Complex/info.json b/data/models/projects/Python_Complex/info.json index 0dc742fe..df068165 100644 --- a/data/models/projects/Python_Complex/info.json +++ b/data/models/projects/Python_Complex/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Python Complex", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "Omar-Belghaouti", "name": "PythonComplex" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/QuranIPFS/info.json b/data/models/projects/QuranIPFS/info.json index 5337dc30..4ef51b93 100644 --- a/data/models/projects/QuranIPFS/info.json +++ b/data/models/projects/QuranIPFS/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "QuranIPFS", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "adelpro", "name": "Quranipfs" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/React_Glassmorphism_Components/info.json b/data/models/projects/React_Glassmorphism_Components/info.json index fb241498..6849965a 100644 --- a/data/models/projects/React_Glassmorphism_Components/info.json +++ b/data/models/projects/React_Glassmorphism_Components/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "React glassmorphism components", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "jusinamine", "name": "react-glassmorphism-components" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/React_Native_Currency_Converter/info.json b/data/models/projects/React_Native_Currency_Converter/info.json index 01575027..fef24b46 100644 --- a/data/models/projects/React_Native_Currency_Converter/info.json +++ b/data/models/projects/React_Native_Currency_Converter/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "ReactNative Currency Converter", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "ilies-space", "name": "currency_converter_rn" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/React_Project_Builder/info.json b/data/models/projects/React_Project_Builder/info.json index 7283a6f4..9eadbbc8 100644 --- a/data/models/projects/React_Project_Builder/info.json +++ b/data/models/projects/React_Project_Builder/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "React and React Native Project Builder", "repositories": [ { @@ -16,5 +17,6 @@ "owner": "Omar-Belghaouti", "name": "react-native-help-create" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/STRM_Test/info.json b/data/models/projects/STRM_Test/info.json index f97a6002..eb672da9 100644 --- a/data/models/projects/STRM_Test/info.json +++ b/data/models/projects/STRM_Test/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "STRM test", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "linuxscout", "name": "strm-tests" } - ] + ], + "tags": ["solve-algerian-problem"] } diff --git a/data/models/projects/System_Monitor/info.json b/data/models/projects/System_Monitor/info.json index 69a0df76..527adf6a 100644 --- a/data/models/projects/System_Monitor/info.json +++ b/data/models/projects/System_Monitor/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "System Monitor", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "ZibanPirate", "name": "sysmon" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Violence_Detection/info.json b/data/models/projects/Violence_Detection/info.json index 36622708..aab7d013 100644 --- a/data/models/projects/Violence_Detection/info.json +++ b/data/models/projects/Violence_Detection/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "violence detection", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "jusinamine", "name": "violence_detection" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Voice_Translator_React_Native/info.json b/data/models/projects/Voice_Translator_React_Native/info.json index 2a0e6349..c34fc836 100644 --- a/data/models/projects/Voice_Translator_React_Native/info.json +++ b/data/models/projects/Voice_Translator_React_Native/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "ReactNative Voice Translator", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "ilies-space", "name": "voiceTranslator-reactNative" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/Weather_Forecast/info.json b/data/models/projects/Weather_Forecast/info.json index 1391db28..e3deb245 100644 --- a/data/models/projects/Weather_Forecast/info.json +++ b/data/models/projects/Weather_Forecast/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Weather Forecast", "repositories": [ { @@ -16,5 +17,6 @@ "owner": "bacloud22", "name": "WeatherVenue" } - ] + ], + "tags": ["by-algerian"] } diff --git a/data/models/projects/list.json b/data/models/projects/list.json index feddc952..06cee989 100644 --- a/data/models/projects/list.json +++ b/data/models/projects/list.json @@ -1,4 +1,4 @@ { "items": "all", - "include": ["name", "repositories"] + "include": ["name", "repositories", "tags"] } diff --git a/data/models/projects/who_want_to_millionaire/info.json b/data/models/projects/who_want_to_millionaire/info.json index 7ef5ca13..50543610 100644 --- a/data/models/projects/who_want_to_millionaire/info.json +++ b/data/models/projects/who_want_to_millionaire/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "Who wants to be a millionaire app mulilingual", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "brahaim360", "name": "who-want-to-be-millionere" } - ] + ], + "tags": ["by-algerian"] } From f0c5798f058b2679099f9eac247d1c51b56dfa89 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 16:22:46 +0100 Subject: [PATCH 3/8] feat: save TagEntity for each project --- api/db/migrations/0001_classy_blindfold.sql | 8 + api/db/migrations/0002_cooing_thena.sql | 13 + api/db/migrations/meta/0001_snapshot.json | 409 +++++++++++++++++ api/db/migrations/meta/0002_snapshot.json | 467 ++++++++++++++++++++ api/db/migrations/meta/_journal.json | 14 + api/src/contributor/table.ts | 2 +- api/src/data/types.ts | 1 + api/src/digest/cron.ts | 20 +- api/src/project/repository.ts | 29 +- api/src/project/table.ts | 24 +- api/src/repository/table.ts | 2 +- api/src/tag/repository.ts | 24 + api/src/tag/table.ts | 15 + packages/models/src/tag/index.ts | 5 + 14 files changed, 1028 insertions(+), 5 deletions(-) create mode 100644 api/db/migrations/0001_classy_blindfold.sql create mode 100644 api/db/migrations/0002_cooing_thena.sql create mode 100644 api/db/migrations/meta/0001_snapshot.json create mode 100644 api/db/migrations/meta/0002_snapshot.json create mode 100644 api/src/tag/repository.ts create mode 100644 api/src/tag/table.ts create mode 100644 packages/models/src/tag/index.ts diff --git a/api/db/migrations/0001_classy_blindfold.sql b/api/db/migrations/0001_classy_blindfold.sql new file mode 100644 index 00000000..a7206c1f --- /dev/null +++ b/api/db/migrations/0001_classy_blindfold.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS "tags" ( + "id" text PRIMARY KEY NOT NULL, + "record_imported_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "run_id" text DEFAULT 'initial-run-id' NOT NULL +); +--> statement-breakpoint +ALTER TABLE "contributors" ALTER COLUMN "run_id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "projects" ALTER COLUMN "run_id" DROP DEFAULT; \ No newline at end of file diff --git a/api/db/migrations/0002_cooing_thena.sql b/api/db/migrations/0002_cooing_thena.sql new file mode 100644 index 00000000..d9eb7303 --- /dev/null +++ b/api/db/migrations/0002_cooing_thena.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS "project_tag_relation" ( + "project_id" text NOT NULL, + "tag_id" text NOT NULL, + "record_imported_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, + "run_id" text DEFAULT 'initial-run-id' NOT NULL, + CONSTRAINT "project_tag_relation_pk" PRIMARY KEY("project_id","tag_id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "project_tag_relation" ADD CONSTRAINT "project_tag_relation_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/api/db/migrations/meta/0001_snapshot.json b/api/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..62b1205d --- /dev/null +++ b/api/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,409 @@ +{ + "id": "20a0174b-3c5f-454d-bf6e-5dc76091cb23", + "prevId": "3685bc1c-7589-45e6-bc50-a8a76537f753", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/0002_snapshot.json b/api/db/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..de7d297a --- /dev/null +++ b/api/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,467 @@ +{ + "id": "429ed010-076d-4544-a9c0-7c9d7d8073ea", + "prevId": "20a0174b-3c5f-454d-bf6e-5dc76091cb23", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.contributions": { + "name": "contributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activity_count": { + "name": "activity_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributions_repository_id_repositories_id_fk": { + "name": "contributions_repository_id_repositories_id_fk", + "tableFrom": "contributions", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributions_contributor_id_contributors_id_fk": { + "name": "contributions_contributor_id_contributors_id_fk", + "tableFrom": "contributions", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributions_url_unique": { + "name": "contributions_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.contributor_repository_relation": { + "name": "contributor_repository_relation", + "schema": "", + "columns": { + "contributor_id": { + "name": "contributor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "contributor_repository_relation_contributor_id_contributors_id_fk": { + "name": "contributor_repository_relation_contributor_id_contributors_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contributor_repository_relation_repository_id_repositories_id_fk": { + "name": "contributor_repository_relation_repository_id_repositories_id_fk", + "tableFrom": "contributor_repository_relation", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contributor_repository_relation_pk": { + "name": "contributor_repository_relation_pk", + "columns": [ + "contributor_id", + "repository_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.contributors": { + "name": "contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contributors_url_unique": { + "name": "contributors_url_unique", + "nullsNotDistinct": false, + "columns": [ + "url" + ] + } + } + }, + "public.project_tag_relation": { + "name": "project_tag_relation", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + } + }, + "indexes": {}, + "foreignKeys": { + "project_tag_relation_project_id_projects_id_fk": { + "name": "project_tag_relation_project_id_projects_id_fk", + "tableFrom": "project_tag_relation", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_tag_relation_pk": { + "name": "project_tag_relation_pk", + "columns": [ + "project_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_project_id_projects_id_fk": { + "name": "repositories_project_id_projects_id_fk", + "tableFrom": "repositories", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_provider_owner_name_unique": { + "name": "repositories_provider_owner_name_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "owner", + "name" + ] + } + } + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_imported_at": { + "name": "record_imported_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "CURRENT_TIMESTAMP" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'initial-run-id'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/db/migrations/meta/_journal.json b/api/db/migrations/meta/_journal.json index 53909155..0369048e 100644 --- a/api/db/migrations/meta/_journal.json +++ b/api/db/migrations/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1727968406636, "tag": "0000_oval_zemo", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1735482300385, + "tag": "0001_classy_blindfold", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1735482799289, + "tag": "0002_cooing_thena", + "breakpoints": true } ] } \ No newline at end of file diff --git a/api/src/contributor/table.ts b/api/src/contributor/table.ts index 7854fe82..e602deb9 100644 --- a/api/src/contributor/table.ts +++ b/api/src/contributor/table.ts @@ -31,7 +31,7 @@ export const contributorRepositoryRelationTable = pgTable( recordImportedAt: text("record_imported_at") .notNull() .default(sql`CURRENT_TIMESTAMP`), - runId: text("run_id").notNull().default("initial-run-id"), + runId: text("run_id").notNull(), score: integer("score").notNull(), }, (table) => ({ diff --git a/api/src/data/types.ts b/api/src/data/types.ts index af426196..88f8c830 100644 --- a/api/src/data/types.ts +++ b/api/src/data/types.ts @@ -8,4 +8,5 @@ export type DataProjectEntity = { owner: string; name: string; }>; + tags?: string[]; }; diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 43feba56..7efe3e12 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -13,6 +13,7 @@ import { ProjectRow } from "src/project/table"; import { RepositoryRepository } from "src/repository/repository"; import { SearchService } from "src/search/service"; import { Service } from "typedi"; +import { TagRepository } from "src/tag/repository"; @Service() export class DigestCron { @@ -28,6 +29,7 @@ export class DigestCron { private readonly contributionsRepository: ContributionRepository, private readonly contributorsRepository: ContributorRepository, private readonly searchService: SearchService, + private readonly tagsRepository: TagRepository, ) { const SentryCronJob = cron.instrumentCron(CronJob, "DigestCron"); new SentryCronJob( @@ -71,6 +73,10 @@ export class DigestCron { this.logger.info({ message: `Digest cron started, runId: ${runId}` }); const projectsFromDataFolder = await this.dataService.listProjects(); + // uncomment during development + // const projectsFromDataFolder = (await this.dataService.listProjects()).filter((p) => + // ["dzcode.io website", "Mishkal", "System Monitor"].includes(p.name), + // ); for (const project of projectsFromDataFolder) { const projectEntity: ProjectRow = { @@ -79,6 +85,10 @@ export class DigestCron { name: project.name, }; const [{ id: projectId }] = await this.projectsRepository.upsert(projectEntity); + for (const tagId of project.tags || []) { + await this.tagsRepository.upsert({ id: tagId, runId }); + await this.projectsRepository.upsertRelationWithTag({ projectId, tagId, runId }); + } await this.searchService.upsert("project", projectEntity); let addedRepositoryCount = 0; @@ -193,16 +203,24 @@ export class DigestCron { if (addedRepositoryCount === 0) { captureException(new Error("Empty project"), { extra: { project } }); + await this.projectsRepository.deleteRelationWithTagByProjectId(projectId); await this.projectsRepository.deleteById(projectId); } } try { - await this.contributorsRepository.deleteAllRelationWithRepositoryButWithRunId(runId); await this.contributionsRepository.deleteAllButWithRunId(runId); + + await this.contributorsRepository.deleteAllRelationWithRepositoryButWithRunId(runId); await this.contributorsRepository.deleteAllButWithRunId(runId); + await this.repositoriesRepository.deleteAllButWithRunId(runId); + + await this.projectsRepository.deleteAllRelationWithTagButWithRunId(runId); await this.projectsRepository.deleteAllButWithRunId(runId); + + await this.tagsRepository.deleteAllButWithRunId(runId); + await Promise.all([ this.searchService.deleteAllButWithRunId("project", runId), this.searchService.deleteAllButWithRunId("contribution", runId), diff --git a/api/src/project/repository.ts b/api/src/project/repository.ts index f99f43e6..f18ba81d 100644 --- a/api/src/project/repository.ts +++ b/api/src/project/repository.ts @@ -6,7 +6,7 @@ import { repositoriesTable } from "src/repository/table"; import { PostgresService } from "src/postgres/service"; import { Service } from "typedi"; -import { ProjectRow, projectsTable } from "./table"; +import { ProjectRow, projectsTable, ProjectTagRelationRow, projectTagRelationTable } from "./table"; @Service() export class ProjectRepository { @@ -203,10 +203,37 @@ export class ProjectRepository { .returning({ id: projectsTable.id }); } + public async upsertRelationWithTag(projectRelationWithTags: ProjectTagRelationRow) { + return await this.postgresService.db + .insert(projectTagRelationTable) + .values(projectRelationWithTags) + .onConflictDoUpdate({ + target: [projectTagRelationTable.projectId, projectTagRelationTable.tagId], + set: projectRelationWithTags, + }) + .returning({ + projectId: projectTagRelationTable.projectId, + tagId: projectTagRelationTable.tagId, + }); + } + + // todo: when deleting Entity, delete all relations with it (apply for project(tag), contributor(repository)) + public async deleteRelationWithTagByProjectId(projectId: string) { + return await this.postgresService.db + .delete(projectTagRelationTable) + .where(eq(projectTagRelationTable.projectId, projectId)); + } + public async deleteById(id: string) { return await this.postgresService.db.delete(projectsTable).where(eq(projectsTable.id, id)); } + public async deleteAllRelationWithTagButWithRunId(runId: string) { + return await this.postgresService.db + .delete(projectTagRelationTable) + .where(ne(projectTagRelationTable.runId, runId)); + } + public async deleteAllButWithRunId(runId: string) { return await this.postgresService.db .delete(projectsTable) diff --git a/api/src/project/table.ts b/api/src/project/table.ts index 4847c1c7..7de9603a 100644 --- a/api/src/project/table.ts +++ b/api/src/project/table.ts @@ -1,6 +1,6 @@ import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { sql } from "drizzle-orm"; -import { pgTable, text } from "drizzle-orm/pg-core"; +import { pgTable, text, primaryKey } from "drizzle-orm/pg-core"; export const projectsTable = pgTable("projects", { id: text("id").notNull().primaryKey(), @@ -14,3 +14,25 @@ export const projectsTable = pgTable("projects", { projectsTable.$inferSelect satisfies ProjectEntity; export type ProjectRow = typeof projectsTable.$inferInsert; + +export const projectTagRelationTable = pgTable( + "project_tag_relation", + { + projectId: text("project_id") + .notNull() + .references(() => projectsTable.id), + tagId: text("tag_id").notNull(), + recordImportedAt: text("record_imported_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + runId: text("run_id").notNull(), + }, + (table) => ({ + pk: primaryKey({ + name: "project_tag_relation_pk", + columns: [table.projectId, table.tagId], + }), + }), +); + +export type ProjectTagRelationRow = typeof projectTagRelationTable.$inferInsert; diff --git a/api/src/repository/table.ts b/api/src/repository/table.ts index 5fa27861..73bde054 100644 --- a/api/src/repository/table.ts +++ b/api/src/repository/table.ts @@ -13,7 +13,7 @@ export const repositoriesTable = pgTable( provider: text("provider").notNull().$type(), owner: text("owner").notNull(), name: text("name").notNull(), - runId: text("run_id").notNull().default("initial-run-id"), + runId: text("run_id").notNull(), projectId: text("project_id") .notNull() .references(() => projectsTable.id), diff --git a/api/src/tag/repository.ts b/api/src/tag/repository.ts new file mode 100644 index 00000000..99db5f58 --- /dev/null +++ b/api/src/tag/repository.ts @@ -0,0 +1,24 @@ +import { ne } from "drizzle-orm"; +import { PostgresService } from "src/postgres/service"; +import { Service } from "typedi"; +import { tagsTable, TagRow } from "./table"; + +@Service() +export class TagRepository { + constructor(private readonly postgresService: PostgresService) {} + + public async upsert(tag: TagRow) { + return await this.postgresService.db + .insert(tagsTable) + .values(tag) + .onConflictDoUpdate({ + target: [tagsTable.id], + set: tag, + }) + .returning({ id: tagsTable.id }); + } + + public async deleteAllButWithRunId(runId: string) { + return await this.postgresService.db.delete(tagsTable).where(ne(tagsTable.runId, runId)); + } +} diff --git a/api/src/tag/table.ts b/api/src/tag/table.ts new file mode 100644 index 00000000..8fe21cb6 --- /dev/null +++ b/api/src/tag/table.ts @@ -0,0 +1,15 @@ +import { TagEntity } from "@dzcode.io/models/dist/tag"; +import { sql } from "drizzle-orm"; +import { pgTable, text } from "drizzle-orm/pg-core"; + +export const tagsTable = pgTable("tags", { + id: text("id").notNull().primaryKey(), + recordImportedAt: text("record_imported_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + runId: text("run_id").notNull(), +}); + +tagsTable.$inferSelect satisfies TagEntity; + +export type TagRow = typeof tagsTable.$inferInsert; diff --git a/packages/models/src/tag/index.ts b/packages/models/src/tag/index.ts new file mode 100644 index 00000000..4ff78e8e --- /dev/null +++ b/packages/models/src/tag/index.ts @@ -0,0 +1,5 @@ +import { BaseEntity } from "src/_base"; + +export type TagEntity = BaseEntity & { + // todo-ZM: add name later when we implement translation +}; From 3bc3b973331ef083ff622a16b89a6d65f826fd23 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 17:47:52 +0100 Subject: [PATCH 4/8] get proj categories from BE and show them on FE --- api/src/digest/cron.ts | 2 + api/src/project/repository.ts | 16 ++++-- api/src/project/types.ts | 2 + .../Algeria_Covid19_Tracker/info.json | 2 +- web/src/components/locale/dictionary.ts | 14 +++-- web/src/pages/projects/index.tsx | 52 +++++++++++++++---- 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 7efe3e12..8b67b088 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -77,6 +77,8 @@ export class DigestCron { // const projectsFromDataFolder = (await this.dataService.listProjects()).filter((p) => // ["dzcode.io website", "Mishkal", "System Monitor"].includes(p.name), // ); + // or uncomment to skip the cron + // if (Math.random()) return; for (const project of projectsFromDataFolder) { const projectEntity: ProjectRow = { diff --git a/api/src/project/repository.ts b/api/src/project/repository.ts index f18ba81d..3e47e56a 100644 --- a/api/src/project/repository.ts +++ b/api/src/project/repository.ts @@ -80,12 +80,14 @@ export class ProjectRepository { public async findForList() { const statement = sql` SELECT - id, - name, + p.id, + p.name, sum(repo_with_stats.contributor_count)::int as total_repo_contributor_count, sum(repo_with_stats.stars)::int as total_repo_stars, sum(repo_with_stats.score)::int as total_repo_score, - ROUND( 100 * sum(repo_with_stats.contributor_count) + 100 * sum(repo_with_stats.stars) + max(repo_with_stats.score) - sum(repo_with_stats.score) / sum(repo_with_stats.contributor_count) )::int as ranking + ROUND( 100 * sum(repo_with_stats.contributor_count) + 100 * sum(repo_with_stats.stars) + max(repo_with_stats.score) - sum(repo_with_stats.score) / sum(repo_with_stats.contributor_count) )::int as ranking, + COALESCE(array_agg(DISTINCT t.id) filter (where t.id is not null), '{}') as tags + FROM ( SELECT @@ -102,9 +104,13 @@ export class ProjectRepository { ${contributorRepositoryRelationTable.repositoryId}, ${repositoriesTable.projectId}, ${repositoriesTable.stars} ) as repo_with_stats JOIN - ${projectsTable} ON ${projectsTable.id} = repo_with_stats.project_id + ${projectsTable} p ON p.id = repo_with_stats.project_id + LEFT JOIN + ${projectTagRelationTable} ptr ON p.id = ptr.project_id + LEFT JOIN + tags t ON ptr.tag_id = t.id GROUP BY - ${projectsTable.id} + p.id ORDER BY ranking DESC `; diff --git a/api/src/project/types.ts b/api/src/project/types.ts index 20a453b2..3e972912 100644 --- a/api/src/project/types.ts +++ b/api/src/project/types.ts @@ -2,6 +2,7 @@ import { ContributionEntity } from "@dzcode.io/models/dist/contribution"; import { ContributorEntity } from "@dzcode.io/models/dist/contributor"; import { ProjectEntity } from "@dzcode.io/models/dist/project"; import { RepositoryEntity } from "@dzcode.io/models/dist/repository"; +import { TagEntity } from "@dzcode.io/models/dist/tag"; import { GeneralResponse } from "src/app/types"; export interface GetProjectsForSitemapResponse extends GeneralResponse { @@ -15,6 +16,7 @@ export interface GetProjectsResponse extends GeneralResponse { totalRepoScore: number; totalRepoStars: number; ranking: number; + tags: TagEntity["id"][]; } >; } diff --git a/data/models/projects/Algeria_Covid19_Tracker/info.json b/data/models/projects/Algeria_Covid19_Tracker/info.json index 4bb87e85..ff546df7 100644 --- a/data/models/projects/Algeria_Covid19_Tracker/info.json +++ b/data/models/projects/Algeria_Covid19_Tracker/info.json @@ -8,5 +8,5 @@ "name": "dz-covid19.com" } ], - "tags": ["by-algerian"] + "tags": ["solve-algerian-problem"] } diff --git a/web/src/components/locale/dictionary.ts b/web/src/components/locale/dictionary.ts index fb6a93da..ac16c770 100644 --- a/web/src/components/locale/dictionary.ts +++ b/web/src/components/locale/dictionary.ts @@ -252,9 +252,17 @@ Besides the open tasks on [/Contribute](/Contribute) page, you can also contribu en: "Browse a growing list of Algerian open-source projects and be up-to-date with the state of open-source software in Algeria, you can also add your project to the list!", ar: "تصفح قائمة متزايدة من المشاريع الجزائرية مفتوحة المصدر وكن على اطلاع دائم بأحدث البرامج مفتوحة المصدر في الجزائر ، كما يمكنك إضافة مشروعك إلى القائمة!", }, - "projects-header-title": { - en: "Open Source Projects", - ar: "مشاريع مفتوحة المصدر", + "projects-tag-solve-algerian-problem": { + en: "Solves Algerian Problem", + ar: "يحل مشكلة جزائرية", + }, + "projects-tag-by-algerian": { + en: "From an Algerian developer", + ar: "من مطور جزائري", + }, + "projects-tag-non-categorized": { + en: "Other", + ar: "أخرى", }, "projects-card-cta-button": { en: "Details", diff --git a/web/src/pages/projects/index.tsx b/web/src/pages/projects/index.tsx index bb2827d0..f20b0a98 100644 --- a/web/src/pages/projects/index.tsx +++ b/web/src/pages/projects/index.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import { GetProjectsResponse } from "@dzcode.io/api/dist/project/types"; +import React, { Fragment, useMemo } from "react"; import { useEffect } from "react"; import { Helmet } from "react-helmet-async"; import { Loading } from "src/components/loading"; import { Locale, useLocale } from "src/components/locale"; +import { DictionaryKeys } from "src/components/locale/dictionary"; import { ProjectCard } from "src/components/project-card"; import { TryAgain } from "src/components/try-again"; import { fetchProjectsListAction } from "src/redux/actions/projects"; @@ -14,6 +16,34 @@ export default function Page(): JSX.Element { const { projectsList } = useAppSelector((state) => state.projectsPage); const dispatch = useAppDispatch(); + const projectsGroupedByTag: Array<{ + tag: string; + projects: GetProjectsResponse["projects"]; + }> = useMemo(() => { + if (projectsList === null || projectsList === "ERROR") return []; + + const projectsGroupedByTag: Record = { + "solve-algerian-problem": [], + "by-algerian": [], + "non-categorized": [], + }; + + projectsList.forEach((project) => { + if (project.tags.includes("solve-algerian-problem")) { + projectsGroupedByTag["solve-algerian-problem"].push(project); + } else if (project.tags.includes("by-algerian")) { + projectsGroupedByTag["by-algerian"].push(project); + } else { + projectsGroupedByTag["non-categorized"].push(project); + } + }); + + return Object.entries(projectsGroupedByTag).map(([tag, projects]) => ({ + tag, + projects, + })); + }, [projectsList]); + useEffect(() => { dispatch(fetchProjectsListAction()); }, [dispatch]); @@ -24,9 +54,6 @@ export default function Page(): JSX.Element { {localize("projects-title")} -

- -

{projectsList === "ERROR" ? ( @@ -40,11 +67,18 @@ export default function Page(): JSX.Element { ) : projectsList === null ? ( ) : ( -
- {projectsList.map((project, projectIndex) => ( - - ))} -
+ projectsGroupedByTag.map((group, groupIndex) => ( + +

+ } /> +

+
+ {group.projects.map((project, projectIndex) => ( + + ))} +
+
+ )) )}
From 13f6952a295041d30b927bb71734d26300570721 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 19:09:09 +0100 Subject: [PATCH 5/8] fix: improve wording for project tags in dictionary --- web/src/components/locale/dictionary.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/locale/dictionary.ts b/web/src/components/locale/dictionary.ts index ac16c770..de763ffd 100644 --- a/web/src/components/locale/dictionary.ts +++ b/web/src/components/locale/dictionary.ts @@ -253,11 +253,11 @@ Besides the open tasks on [/Contribute](/Contribute) page, you can also contribu ar: "تصفح قائمة متزايدة من المشاريع الجزائرية مفتوحة المصدر وكن على اطلاع دائم بأحدث البرامج مفتوحة المصدر في الجزائر ، كما يمكنك إضافة مشروعك إلى القائمة!", }, "projects-tag-solve-algerian-problem": { - en: "Solves Algerian Problem", + en: "Solves an Algerian Problem", ar: "يحل مشكلة جزائرية", }, "projects-tag-by-algerian": { - en: "From an Algerian developer", + en: "By an Algerian developer", ar: "من مطور جزائري", }, "projects-tag-non-categorized": { From b996099f942f0df17fd6cfaf3bb57e67d8ce6bfb Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 20:54:33 +0100 Subject: [PATCH 6/8] feat: tag to dzcode.io website as solve-algerian-problem --- data/models/projects/dzcode.io_website/info.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/models/projects/dzcode.io_website/info.json b/data/models/projects/dzcode.io_website/info.json index d2b9bf8c..9f55a8f7 100644 --- a/data/models/projects/dzcode.io_website/info.json +++ b/data/models/projects/dzcode.io_website/info.json @@ -1,4 +1,5 @@ { + "$schema": "../../schemas/project.json", "name": "dzcode.io website", "repositories": [ { @@ -6,5 +7,6 @@ "owner": "dzcode-io", "name": "dzcode.io" } - ] + ], + "tags": ["solve-algerian-problem"] } From fbca05ea964f47a0b1cfcd00e7ffb1b82d742e4d Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 21:22:08 +0100 Subject: [PATCH 7/8] fix: add TODO comment to make project listing configurable --- api/src/digest/cron.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/digest/cron.ts b/api/src/digest/cron.ts index 8b67b088..63f76483 100644 --- a/api/src/digest/cron.ts +++ b/api/src/digest/cron.ts @@ -73,6 +73,7 @@ export class DigestCron { this.logger.info({ message: `Digest cron started, runId: ${runId}` }); const projectsFromDataFolder = await this.dataService.listProjects(); + // todo-ZM: make this configurable // uncomment during development // const projectsFromDataFolder = (await this.dataService.listProjects()).filter((p) => // ["dzcode.io website", "Mishkal", "System Monitor"].includes(p.name), From 70b3c8ef2245c821c5979761ceadd9a635eb4df1 Mon Sep 17 00:00:00 2001 From: Zakaria Mansouri Date: Sun, 29 Dec 2024 21:22:18 +0100 Subject: [PATCH 8/8] fix: prevent rendering empty project groups in the project listing --- web/src/pages/projects/index.tsx | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/web/src/pages/projects/index.tsx b/web/src/pages/projects/index.tsx index f20b0a98..04045810 100644 --- a/web/src/pages/projects/index.tsx +++ b/web/src/pages/projects/index.tsx @@ -67,18 +67,23 @@ export default function Page(): JSX.Element { ) : projectsList === null ? ( ) : ( - projectsGroupedByTag.map((group, groupIndex) => ( - -

- } /> -

-
- {group.projects.map((project, projectIndex) => ( - - ))} -
-
- )) + projectsGroupedByTag.map( + (group, groupIndex) => + group.projects.length > 0 && ( + +

+ } + /> +

+
+ {group.projects.map((project, projectIndex) => ( + + ))} +
+
+ ), + ) )}