mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
feat: add PostgreSQL database support and development scripts
- Added pg and @types/pg dependencies for PostgreSQL integration - Created database testing and backend initialization scripts - Changed dev server port to 3002 to avoid conflicts
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=repohub
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=your_password_here
|
||||
|
||||
# Next.js Configuration
|
||||
NEXTAUTH_URL=http://localhost:3002
|
||||
NEXTAUTH_SECRET=your_secret_here
|
||||
|
||||
# API Configuration
|
||||
API_BASE_URL=http://localhost:3000/api
|
||||
|
||||
# Sync Configuration
|
||||
SYNC_ENABLED=true
|
||||
SYNC_INTERVAL=3600000 # 1 hour in milliseconds
|
||||
+7
-3
@@ -3,11 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 3002",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"test:db": "node scripts/test-db.js",
|
||||
"init:backend": "chmod +x scripts/init-backend.sh && ./scripts/init-backend.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.0.4",
|
||||
@@ -25,13 +27,15 @@
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"react-query": "^3.39.3",
|
||||
"zustand": "^4.4.7"
|
||||
"zustand": "^4.4.7",
|
||||
"pg": "^8.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/pg": "^8.10.9",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.3.0",
|
||||
|
||||
Generated
+122
@@ -38,6 +38,9 @@ importers:
|
||||
next:
|
||||
specifier: 14.0.4
|
||||
version: 14.0.4([email protected]([email protected]))([email protected])
|
||||
pg:
|
||||
specifier: ^8.11.3
|
||||
version: 8.16.3
|
||||
react:
|
||||
specifier: ^18
|
||||
version: 18.3.1
|
||||
@@ -60,6 +63,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.19.24
|
||||
'@types/pg':
|
||||
specifier: ^8.10.9
|
||||
version: 8.15.6
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.3.26
|
||||
@@ -620,6 +626,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||
|
||||
@@ -1769,6 +1778,40 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1843,6 +1886,22 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2029,6 +2088,10 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||
|
||||
@@ -2278,6 +2341,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2794,6 +2861,12 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/[email protected]':
|
||||
dependencies:
|
||||
'@types/node': 20.19.24
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected](@types/[email protected])':
|
||||
@@ -4111,6 +4184,41 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
pg: 8.16.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.0
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pg-connection-string: 2.9.1
|
||||
pg-pool: 3.10.1([email protected])
|
||||
pg-protocol: 1.10.3
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.2.7
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4166,6 +4274,16 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4371,6 +4489,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -4719,6 +4839,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@types/[email protected])([email protected]):
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
# RepoHub Backend Initialization Script
|
||||
# This script sets up the database and initializes the backend
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Initializing RepoHub Backend..."
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
DB_HOST=${DB_HOST:-localhost}
|
||||
DB_PORT=${DB_PORT:-5432}
|
||||
DB_NAME=${DB_NAME:-repohub}
|
||||
DB_USER=${DB_USER:-postgres}
|
||||
DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
|
||||
echo -e "${BLUE}📋 Configuration:${NC}"
|
||||
echo " Host: $DB_HOST"
|
||||
echo " Port: $DB_PORT"
|
||||
echo " Database: $DB_NAME"
|
||||
echo " User: $DB_USER"
|
||||
|
||||
# Check if PostgreSQL is running
|
||||
echo -e "\n${YELLOW}🔍 Checking PostgreSQL connection...${NC}"
|
||||
if ! pg_isready -h $DB_HOST -p $DB_PORT; then
|
||||
echo -e "${RED}❌ PostgreSQL is not running on $DB_HOST:$DB_PORT${NC}"
|
||||
echo "Please start PostgreSQL and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ PostgreSQL is running${NC}"
|
||||
|
||||
# Check if database exists
|
||||
echo -e "\n${YELLOW}🗄️ Checking database...${NC}"
|
||||
if psql -h $DB_HOST -p $DB_PORT -U $DB_USER -lqt | cut -d \| -f 1 | grep -qw $DB_NAME; then
|
||||
echo -e "${GREEN}✅ Database '$DB_NAME' already exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Database '$DB_NAME' does not exist. Creating...${NC}"
|
||||
createdb -h $DB_HOST -p $DB_PORT -U $DB_USER $DB_NAME
|
||||
echo -e "${GREEN}✅ Database '$DB_NAME' created${NC}"
|
||||
fi
|
||||
|
||||
# Check if tables exist
|
||||
echo -e "\n${YELLOW}📊 Checking database tables...${NC}"
|
||||
TABLES_EXIST=$(psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'platforms');")
|
||||
|
||||
if [ "$TABLES_EXIST" = "t" ]; then
|
||||
echo -e "${GREEN}✅ Database tables already exist${NC}"
|
||||
read -p "Do you want to reset the database? This will delete all data. (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}🔄 Resetting database...${NC}"
|
||||
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f src/lib/database/schema.sql
|
||||
echo -e "${GREEN}✅ Database reset completed${NC}"
|
||||
else
|
||||
echo -e "${BLUE}ℹ️ Keeping existing data${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Database tables do not exist. Creating schema...${NC}"
|
||||
psql -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f src/lib/database/schema.sql
|
||||
echo -e "${GREEN}✅ Database schema created${NC}"
|
||||
fi
|
||||
|
||||
# Check if node_modules exists
|
||||
echo -e "\n${YELLOW}📦 Checking dependencies...${NC}"
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo -e "${YELLOW}⚠️ Dependencies not found. Installing...${NC}"
|
||||
pnpm install
|
||||
echo -e "${GREEN}✅ Dependencies installed${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Dependencies already exist${NC}"
|
||||
fi
|
||||
|
||||
# Check if .env file exists
|
||||
echo -e "\n${YELLOW}⚙️ Checking environment configuration...${NC}"
|
||||
if [ ! -f ".env" ]; then
|
||||
echo -e "${YELLOW}⚠️ .env file not found. Creating from example...${NC}"
|
||||
cp .env.example .env
|
||||
echo -e "${GREEN}✅ .env file created${NC}"
|
||||
echo -e "${YELLOW}⚠️ Please edit .env file with your database credentials${NC}"
|
||||
echo " Current configuration:"
|
||||
echo " DB_HOST=$DB_HOST"
|
||||
echo " DB_PORT=$DB_PORT"
|
||||
echo " DB_NAME=$DB_NAME"
|
||||
echo " DB_USER=$DB_USER"
|
||||
echo " DB_PASSWORD=$DB_PASSWORD"
|
||||
else
|
||||
echo -e "${GREEN}✅ .env file already exists${NC}"
|
||||
fi
|
||||
|
||||
# Test database connection
|
||||
echo -e "\n${YELLOW}🔗 Testing database connection...${NC}"
|
||||
if pnpm run test:db 2>/dev/null; then
|
||||
echo -e "${GREEN}✅ Database connection successful${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Database connection test failed. This is expected if test script doesn't exist yet.${NC}"
|
||||
fi
|
||||
|
||||
# Display next steps
|
||||
echo -e "\n${GREEN}🎉 Backend initialization completed!${NC}"
|
||||
echo -e "\n${BLUE}📋 Next steps:${NC}"
|
||||
echo "1. Review and update .env file if needed"
|
||||
echo "2. Start the development server:"
|
||||
echo " ${YELLOW}pnpm dev${NC}"
|
||||
echo "3. Test the API endpoints:"
|
||||
echo " ${YELLOW}curl http://localhost:3000/api/platforms${NC}"
|
||||
echo "4. Sync Ubuntu packages:"
|
||||
echo " ${YELLOW}curl -X POST http://localhost:3000/api/sync -H 'Content-Type: application/json' -d '{\"platform_id\": \"ubuntu\"}'${NC}"
|
||||
|
||||
echo -e "\n${GREEN}✨ Ready to start development!${NC}"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Database connection test script
|
||||
const { testConnection } = require('../src/lib/database/config');
|
||||
|
||||
async function testDatabaseConnection() {
|
||||
console.log('🔗 Testing database connection...');
|
||||
|
||||
try {
|
||||
const success = await testConnection();
|
||||
|
||||
if (success) {
|
||||
console.log('✅ Database connection successful!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('❌ Database connection failed!');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testDatabaseConnection();
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { PackageService } from '@/services/packageService'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const packageData = await PackageService.getById(params.id)
|
||||
|
||||
if (!packageData) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Package not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(packageData)
|
||||
} catch (error) {
|
||||
console.error('Error fetching package:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch package' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const packageData = await PackageService.update(params.id, body)
|
||||
|
||||
if (!packageData) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Package not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(packageData)
|
||||
} catch (error) {
|
||||
console.error('Error updating package:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update package' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const success = await PackageService.delete(params.id)
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Package not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting package:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete package' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { PackageService } from '@/services/packageService'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
// Parse query parameters
|
||||
const filter = {
|
||||
platform_id: searchParams.get('platform_id') || undefined,
|
||||
category_id: searchParams.get('category_id') ?
|
||||
parseInt(searchParams.get('category_id')!) : undefined,
|
||||
type: searchParams.get('type') as 'gui' | 'cli' | undefined,
|
||||
repository: searchParams.get('repository') as 'official' | 'third-party' | undefined,
|
||||
search: searchParams.get('search') || undefined,
|
||||
limit: searchParams.get('limit') ?
|
||||
parseInt(searchParams.get('limit')!) : undefined,
|
||||
offset: searchParams.get('offset') ?
|
||||
parseInt(searchParams.get('offset')!) : undefined,
|
||||
sort_by: searchParams.get('sort_by') as
|
||||
'name' | 'popularity_score' | 'last_updated' | 'downloads_count' | undefined,
|
||||
sort_order: searchParams.get('sort_order') as 'asc' | 'desc' | undefined
|
||||
}
|
||||
|
||||
const result = await PackageService.getMany(filter)
|
||||
|
||||
return NextResponse.json({
|
||||
packages: result.packages,
|
||||
total: result.total,
|
||||
limit: filter.limit,
|
||||
offset: filter.offset
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching packages:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch packages' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const packageData = await PackageService.create(body)
|
||||
return NextResponse.json(packageData, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating package:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create package' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { PlatformService } from '@/services/platformService'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const platform = await PlatformService.getById(params.id)
|
||||
|
||||
if (!platform) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Platform not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(platform)
|
||||
} catch (error) {
|
||||
console.error('Error fetching platform:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch platform' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const platform = await PlatformService.update(params.id, body)
|
||||
|
||||
if (!platform) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Platform not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(platform)
|
||||
} catch (error) {
|
||||
console.error('Error updating platform:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update platform' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const success = await PlatformService.delete(params.id)
|
||||
|
||||
if (!success) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Platform not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting platform:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete platform' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { PlatformService } from '@/services/platformService'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const platforms = await PlatformService.getAll()
|
||||
return NextResponse.json(platforms)
|
||||
} catch (error) {
|
||||
console.error('Error fetching platforms:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch platforms' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const platform = await PlatformService.create(body)
|
||||
return NextResponse.json(platform, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Error creating platform:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create platform' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { MetadataFetcher } from '@/services/metadataFetcher'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { platform_id, all_platforms } = body
|
||||
|
||||
if (all_platforms) {
|
||||
// Sync all platforms
|
||||
await MetadataFetcher.syncAllPlatforms()
|
||||
return NextResponse.json({
|
||||
message: 'All platforms synced successfully',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} else if (platform_id) {
|
||||
// Sync specific platform
|
||||
await MetadataFetcher.syncPlatform(platform_id)
|
||||
return NextResponse.json({
|
||||
message: `Platform ${platform_id} synced successfully`,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either platform_id or all_platforms must be specified' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Sync failed', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Return sync status (would need to implement status tracking)
|
||||
return NextResponse.json({
|
||||
status: 'ready',
|
||||
last_sync: null,
|
||||
platforms: ['ubuntu', 'fedora', 'arch', 'windows', 'macos']
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error getting sync status:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get sync status' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Pool, PoolConfig } from 'pg'
|
||||
|
||||
// Database configuration
|
||||
const dbConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'repohub',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
max: 20, // Maximum number of connections in the pool
|
||||
idleTimeoutMillis: 30000, // How long a client is allowed to remain idle before being closed
|
||||
connectionTimeoutMillis: 2000, // How long to wait when connecting a new client
|
||||
}
|
||||
|
||||
// Create connection pool
|
||||
const pool = new Pool(dbConfig)
|
||||
|
||||
// Test database connection
|
||||
export async function testConnection() {
|
||||
try {
|
||||
const client = await pool.connect()
|
||||
await client.query('SELECT NOW()')
|
||||
client.release()
|
||||
console.log('✅ Database connection successful')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for queries
|
||||
export async function query(text: string, params?: any[]) {
|
||||
const start = Date.now()
|
||||
try {
|
||||
const res = await pool.query(text, params)
|
||||
const duration = Date.now() - start
|
||||
console.log('📊 Query executed', { text, duration, rows: res.rowCount })
|
||||
return res
|
||||
} catch (error) {
|
||||
console.error('❌ Query failed', { text, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get a single client for transactions
|
||||
export async function getClient() {
|
||||
return await pool.connect()
|
||||
}
|
||||
|
||||
// Close all connections
|
||||
export async function closePool() {
|
||||
await pool.end()
|
||||
console.log('🔌 Database connection pool closed')
|
||||
}
|
||||
|
||||
export { pool }
|
||||
export default pool
|
||||
@@ -0,0 +1,140 @@
|
||||
-- RepoHub Database Schema
|
||||
-- PostgreSQL schema for package metadata management
|
||||
|
||||
-- Platforms table
|
||||
CREATE TABLE platforms (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
package_manager VARCHAR(50) NOT NULL,
|
||||
icon VARCHAR(10),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Categories table
|
||||
CREATE TABLE categories (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Licenses table
|
||||
CREATE TABLE licenses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
url VARCHAR(255),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Packages table
|
||||
CREATE TABLE packages (
|
||||
id VARCHAR(100) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
version VARCHAR(50),
|
||||
platform_id VARCHAR(50) NOT NULL REFERENCES platforms(id),
|
||||
category_id INTEGER REFERENCES categories(id),
|
||||
license_id INTEGER REFERENCES licenses(id),
|
||||
type VARCHAR(10) CHECK (type IN ('gui', 'cli')),
|
||||
repository VARCHAR(20) CHECK (repository IN ('official', 'third-party')),
|
||||
homepage_url VARCHAR(255),
|
||||
download_url VARCHAR(255),
|
||||
last_updated TIMESTAMP WITH TIME ZONE,
|
||||
downloads_count INTEGER DEFAULT 0,
|
||||
popularity_score INTEGER CHECK (popularity_score >= 0 AND popularity_score <= 100),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Package tags junction table
|
||||
CREATE TABLE package_tags (
|
||||
package_id VARCHAR(50) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
tag VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
PRIMARY KEY (package_id, tag)
|
||||
);
|
||||
|
||||
-- Package dependencies junction table
|
||||
CREATE TABLE package_dependencies (
|
||||
package_id VARCHAR(50) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
dependency_id VARCHAR(50) REFERENCES packages(id) ON DELETE CASCADE,
|
||||
dependency_type VARCHAR(20) CHECK (dependency_type IN ('required', 'optional', 'recommended')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
PRIMARY KEY (package_id, dependency_id)
|
||||
);
|
||||
|
||||
-- Metadata sync log table
|
||||
CREATE TABLE metadata_sync_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
platform_id VARCHAR(50) REFERENCES platforms(id),
|
||||
sync_type VARCHAR(20) CHECK (sync_type IN ('full', 'incremental')),
|
||||
status VARCHAR(20) CHECK (status IN ('running', 'completed', 'failed')),
|
||||
packages_processed INTEGER DEFAULT 0,
|
||||
packages_added INTEGER DEFAULT 0,
|
||||
packages_updated INTEGER DEFAULT 0,
|
||||
packages_removed INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
completed_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_packages_platform_id ON packages(platform_id);
|
||||
CREATE INDEX idx_packages_category_id ON packages(category_id);
|
||||
CREATE INDEX idx_packages_type ON packages(type);
|
||||
CREATE INDEX idx_packages_repository ON packages(repository);
|
||||
CREATE INDEX idx_packages_is_active ON packages(is_active);
|
||||
CREATE INDEX idx_packages_last_updated ON packages(last_updated);
|
||||
CREATE INDEX idx_packages_popularity_score ON packages(popularity_score DESC);
|
||||
CREATE INDEX idx_packages_name ON packages USING gin(to_tsvector('english', name));
|
||||
CREATE INDEX idx_packages_description ON packages USING gin(to_tsvector('english', description));
|
||||
|
||||
-- Trigger to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_platforms_updated_at BEFORE UPDATE ON platforms
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_packages_updated_at BEFORE UPDATE ON packages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Initial data
|
||||
INSERT INTO platforms (id, name, package_manager, icon) VALUES
|
||||
('ubuntu', 'Ubuntu/Debian', 'apt', '🐧'),
|
||||
('fedora', 'Fedora', 'dnf', '🎩'),
|
||||
('arch', 'Arch Linux', 'pacman', '🏛️'),
|
||||
('windows', 'Windows', 'winget', '🪟'),
|
||||
('macos', 'macOS', 'homebrew', '🍎');
|
||||
|
||||
INSERT INTO categories (name, description) VALUES
|
||||
('Development', 'Software development tools and IDEs'),
|
||||
('Internet', 'Web browsers, email clients, and network tools'),
|
||||
('Multimedia', 'Audio, video, and graphics applications'),
|
||||
('System', 'System administration and utilities'),
|
||||
('Communication', 'Chat, VoIP, and messaging applications'),
|
||||
('Office', 'Productivity and office applications'),
|
||||
('Graphics', 'Image editing and design tools'),
|
||||
('Games', 'Games and gaming-related software'),
|
||||
('Science', 'Scientific and educational software'),
|
||||
('Utilities', 'General utility applications');
|
||||
|
||||
INSERT INTO licenses (name, url) VALUES
|
||||
('MIT', 'https://opensource.org/licenses/MIT'),
|
||||
('GPL-2.0', 'https://opensource.org/licenses/GPL-2.0'),
|
||||
('GPL-3.0', 'https://opensource.org/licenses/GPL-3.0'),
|
||||
('Apache-2.0', 'https://opensource.org/licenses/Apache-2.0'),
|
||||
('BSD-2-Clause', 'https://opensource.org/licenses/BSD-2-Clause'),
|
||||
('BSD-3-Clause', 'https://opensource.org/licenses/BSD-3-Clause'),
|
||||
('MPL-2.0', 'https://opensource.org/licenses/MPL-2.0'),
|
||||
('Proprietary', NULL),
|
||||
('LGPL-2.1', 'https://opensource.org/licenses/LGPL-2.1'),
|
||||
('LGPL-3.0', 'https://opensource.org/licenses/LGPL-3.0');
|
||||
@@ -0,0 +1,86 @@
|
||||
export interface Package {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
version?: string
|
||||
platform_id: string
|
||||
category_id?: number
|
||||
license_id?: number
|
||||
type?: 'gui' | 'cli'
|
||||
repository?: 'official' | 'third-party'
|
||||
homepage_url?: string
|
||||
download_url?: string
|
||||
last_updated?: Date
|
||||
downloads_count?: number
|
||||
popularity_score?: number
|
||||
is_active: boolean
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
|
||||
// Joined fields
|
||||
platform?: Platform
|
||||
category?: Category
|
||||
license?: License
|
||||
tags?: string[]
|
||||
dependencies?: Package[]
|
||||
}
|
||||
|
||||
export interface Platform {
|
||||
id: string
|
||||
name: string
|
||||
package_manager: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface License {
|
||||
id: number
|
||||
name: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface CreatePackageInput {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
version?: string
|
||||
platform_id: string
|
||||
category_id?: number
|
||||
license_id?: number
|
||||
type?: 'gui' | 'cli'
|
||||
repository?: 'official' | 'third-party'
|
||||
homepage_url?: string
|
||||
download_url?: string
|
||||
popularity_score?: number
|
||||
}
|
||||
|
||||
export interface UpdatePackageInput {
|
||||
name?: string
|
||||
description?: string
|
||||
version?: string
|
||||
category_id?: number
|
||||
license_id?: number
|
||||
type?: 'gui' | 'cli'
|
||||
repository?: 'official' | 'third-party'
|
||||
homepage_url?: string
|
||||
download_url?: string
|
||||
popularity_score?: number
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
export interface PackageFilter {
|
||||
platform_id?: string
|
||||
category_id?: number
|
||||
type?: 'gui' | 'cli'
|
||||
repository?: 'official' | 'third-party'
|
||||
search?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
sort_by?: 'name' | 'popularity_score' | 'last_updated' | 'downloads_count'
|
||||
sort_order?: 'asc' | 'desc'
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface Platform {
|
||||
id: string
|
||||
name: string
|
||||
package_manager: string
|
||||
icon?: string
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export interface CreatePlatformInput {
|
||||
id: string
|
||||
name: string
|
||||
package_manager: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface UpdatePlatformInput {
|
||||
name?: string
|
||||
package_manager?: string
|
||||
icon?: string
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { PackageService } from './packageService'
|
||||
import { PlatformService } from './platformService'
|
||||
import { CreatePackageInput } from '@/models/Package'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
export class MetadataFetcher {
|
||||
// Fetch Ubuntu/Debian package metadata
|
||||
static async fetchUbuntuPackages(): Promise<void> {
|
||||
console.log('🔄 Starting Ubuntu/Debian package metadata fetch...')
|
||||
|
||||
try {
|
||||
// Update package lists
|
||||
console.log('📦 Updating apt package lists...')
|
||||
await execAsync('apt-get update')
|
||||
|
||||
// Get package list
|
||||
console.log('📋 Getting package list...')
|
||||
const { stdout } = await execAsync('apt-cache dumpavail')
|
||||
|
||||
// Parse packages
|
||||
const packages = this.parseAptPackages(stdout)
|
||||
console.log(`📊 Found ${packages.length} packages`)
|
||||
|
||||
// Store in database
|
||||
await this.storePackages('ubuntu', packages)
|
||||
|
||||
console.log('✅ Ubuntu/Debian package metadata fetch completed')
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching Ubuntu packages:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Parse apt-cache dumpavail output
|
||||
private static parseAptPackages(output: string): CreatePackageInput[] {
|
||||
const packages: CreatePackageInput[] = []
|
||||
const blocks = output.split('\n\n')
|
||||
|
||||
for (const block of blocks) {
|
||||
if (!block.trim()) continue
|
||||
|
||||
const pkg: any = {}
|
||||
const lines = block.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('Package: ')) {
|
||||
pkg.id = line.substring(9).trim()
|
||||
pkg.name = pkg.id
|
||||
} else if (line.startsWith('Description: ')) {
|
||||
pkg.description = line.substring(13).trim()
|
||||
} else if (line.startsWith('Version: ')) {
|
||||
pkg.version = line.substring(9).trim()
|
||||
} else if (line.startsWith('Homepage: ')) {
|
||||
pkg.homepage_url = line.substring(10).trim()
|
||||
}
|
||||
}
|
||||
|
||||
if (pkg.id && pkg.name) {
|
||||
packages.push({
|
||||
...pkg,
|
||||
platform_id: 'ubuntu',
|
||||
type: this.determinePackageType(pkg.name, pkg.description),
|
||||
repository: 'official',
|
||||
popularity_score: Math.floor(Math.random() * 100) // Placeholder
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
// Determine if package is GUI or CLI
|
||||
private static determinePackageType(name: string, description?: string): 'gui' | 'cli' {
|
||||
const guiKeywords = [
|
||||
'gui', 'gtk', 'qt', 'x11', 'desktop', 'window', 'display',
|
||||
'graphical', 'visual', 'image', 'video', 'audio', 'media',
|
||||
'browser', 'editor', 'viewer', 'player', 'manager'
|
||||
]
|
||||
|
||||
const cliKeywords = [
|
||||
'cli', 'command', 'terminal', 'console', 'shell', 'bash',
|
||||
'tool', 'utility', 'daemon', 'service', 'server', 'client'
|
||||
]
|
||||
|
||||
const searchText = `${name} ${description || ''}`.toLowerCase()
|
||||
|
||||
for (const keyword of guiKeywords) {
|
||||
if (searchText.includes(keyword)) return 'gui'
|
||||
}
|
||||
|
||||
for (const keyword of cliKeywords) {
|
||||
if (searchText.includes(keyword)) return 'cli'
|
||||
}
|
||||
|
||||
// Default to CLI for system packages
|
||||
return 'cli'
|
||||
}
|
||||
|
||||
// Store packages in database
|
||||
private static async storePackages(platformId: string, packages: CreatePackageInput[]): Promise<void> {
|
||||
console.log(`💾 Storing ${packages.length} packages for platform ${platformId}...`)
|
||||
|
||||
let added = 0
|
||||
let updated = 0
|
||||
|
||||
for (const pkg of packages) {
|
||||
try {
|
||||
// Check if package exists
|
||||
const existing = await PackageService.getById(pkg.id)
|
||||
|
||||
if (existing) {
|
||||
// Update existing package
|
||||
await PackageService.update(pkg.id, {
|
||||
description: pkg.description,
|
||||
version: pkg.version,
|
||||
homepage_url: pkg.homepage_url,
|
||||
type: pkg.type,
|
||||
repository: pkg.repository,
|
||||
popularity_score: pkg.popularity_score
|
||||
})
|
||||
updated++
|
||||
} else {
|
||||
// Create new package
|
||||
await PackageService.create(pkg)
|
||||
added++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error storing package ${pkg.id}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📈 Added: ${added}, Updated: ${updated}`)
|
||||
}
|
||||
|
||||
// Fetch package categories from Ubuntu
|
||||
static async fetchUbuntuCategories(): Promise<void> {
|
||||
console.log('🏷️ Fetching Ubuntu package categories...')
|
||||
|
||||
try {
|
||||
// Get sections from apt-cache
|
||||
const { stdout } = await execAsync('apt-cache dump | grep "^Section:" | sort | uniq')
|
||||
|
||||
const categories = stdout
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => line.replace('Section:', '').trim())
|
||||
.filter(category => category && category !== 'unknown')
|
||||
|
||||
console.log(`📋 Found ${categories.length} categories`)
|
||||
|
||||
// Store categories in database (would need CategoryService)
|
||||
for (const category of categories) {
|
||||
console.log(` - ${category}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching Ubuntu categories:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Get popular packages from Ubuntu popularity contest
|
||||
static async getPopularUbuntuPackages(limit: number = 100): Promise<string[]> {
|
||||
try {
|
||||
// Try to get popularity contest data
|
||||
const { stdout } = await execAsync('popularity-contest -a 2>/dev/null || echo ""')
|
||||
|
||||
if (stdout) {
|
||||
const packages = stdout
|
||||
.split('\n')
|
||||
.filter(line => line.includes('POP-CON-Package'))
|
||||
.map(line => line.split(' ')[1])
|
||||
.slice(0, limit)
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
// Fallback: return some common packages
|
||||
return [
|
||||
'curl', 'wget', 'git', 'vim', 'nano', 'firefox', 'chromium-browser',
|
||||
'libreoffice', 'gimp', 'vlc', 'audacity', 'thunderbird', 'code'
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('Error getting popular packages:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Sync metadata for a specific platform
|
||||
static async syncPlatform(platformId: string): Promise<void> {
|
||||
console.log(`🔄 Starting metadata sync for platform: ${platformId}`)
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
switch (platformId) {
|
||||
case 'ubuntu':
|
||||
await this.fetchUbuntuPackages()
|
||||
break
|
||||
case 'fedora':
|
||||
// TODO: Implement Fedora metadata fetching
|
||||
console.log('⏳ Fedora metadata fetching not yet implemented')
|
||||
break
|
||||
case 'arch':
|
||||
// TODO: Implement Arch metadata fetching
|
||||
console.log('⏳ Arch metadata fetching not yet implemented')
|
||||
break
|
||||
case 'windows':
|
||||
// TODO: Implement Windows metadata fetching
|
||||
console.log('⏳ Windows metadata fetching not yet implemented')
|
||||
break
|
||||
case 'macos':
|
||||
// TODO: Implement macOS metadata fetching
|
||||
console.log('⏳ macOS metadata fetching not yet implemented')
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platformId}`)
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
console.log(`✅ Sync completed for ${platformId} in ${duration}ms`)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Sync failed for ${platformId}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Sync all platforms
|
||||
static async syncAllPlatforms(): Promise<void> {
|
||||
console.log('🔄 Starting metadata sync for all platforms...')
|
||||
|
||||
const platforms = await PlatformService.getAll()
|
||||
|
||||
for (const platform of platforms) {
|
||||
try {
|
||||
await this.syncPlatform(platform.id)
|
||||
} catch (error) {
|
||||
console.error(`Failed to sync ${platform.name}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ All platforms sync completed')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import { query } from '@/lib/database/config'
|
||||
import { Package, CreatePackageInput, UpdatePackageInput, PackageFilter } from '@/models/Package'
|
||||
|
||||
export class PackageService {
|
||||
// Get packages with filtering and pagination
|
||||
static async getMany(filter: PackageFilter = {}): Promise<{ packages: Package[], total: number }> {
|
||||
const {
|
||||
platform_id,
|
||||
category_id,
|
||||
type,
|
||||
repository,
|
||||
search,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
sort_by = 'name',
|
||||
sort_order = 'asc'
|
||||
} = filter
|
||||
|
||||
// Build WHERE clause
|
||||
const whereConditions = []
|
||||
const values = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (platform_id) {
|
||||
whereConditions.push(`p.platform_id = $${paramIndex++}`)
|
||||
values.push(platform_id)
|
||||
}
|
||||
|
||||
if (category_id) {
|
||||
whereConditions.push(`p.category_id = $${paramIndex++}`)
|
||||
values.push(category_id)
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push(`p.type = $${paramIndex++}`)
|
||||
values.push(type)
|
||||
}
|
||||
|
||||
if (repository) {
|
||||
whereConditions.push(`p.repository = $${paramIndex++}`)
|
||||
values.push(repository)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
whereConditions.push(`(
|
||||
to_tsvector('english', p.name) @@ to_tsquery('english', $${paramIndex}) OR
|
||||
to_tsvector('english', p.description) @@ to_tsquery('english', $${paramIndex})
|
||||
)`)
|
||||
values.push(search.split(' ').join(' & '))
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
whereConditions.push('p.is_active = true')
|
||||
|
||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''
|
||||
|
||||
// Build ORDER BY clause
|
||||
const validSortFields = ['name', 'popularity_score', 'last_updated', 'downloads_count']
|
||||
const sortField = validSortFields.includes(sort_by || '') ? sort_by : 'name'
|
||||
const sortDirection = sort_order === 'desc' ? 'DESC' : 'ASC'
|
||||
const orderClause = `ORDER BY p.${sortField} ${sortDirection}`
|
||||
|
||||
// Get total count
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM packages p
|
||||
${whereClause}
|
||||
`
|
||||
const countResult = await query(countQuery, values)
|
||||
const total = parseInt(countResult.rows[0].total)
|
||||
|
||||
// Get packages with joins
|
||||
const packagesQuery = `
|
||||
SELECT
|
||||
p.*,
|
||||
pl.name as platform_name,
|
||||
pl.package_manager as platform_package_manager,
|
||||
pl.icon as platform_icon,
|
||||
c.name as category_name,
|
||||
l.name as license_name,
|
||||
l.url as license_url,
|
||||
COALESCE(
|
||||
ARRAY_AGG(DISTINCT pt.tag) FILTER (WHERE pt.tag IS NOT NULL),
|
||||
ARRAY[]::VARCHAR[]
|
||||
) as tags
|
||||
FROM packages p
|
||||
LEFT JOIN platforms pl ON p.platform_id = pl.id
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
LEFT JOIN licenses l ON p.license_id = l.id
|
||||
LEFT JOIN package_tags pt ON p.id = pt.package_id
|
||||
${whereClause}
|
||||
GROUP BY p.id, pl.name, pl.package_manager, pl.icon, c.name, l.name, l.url
|
||||
${orderClause}
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
||||
`
|
||||
|
||||
values.push(limit, offset)
|
||||
const packagesResult = await query(packagesQuery, values)
|
||||
|
||||
// Transform the results
|
||||
const packages = packagesResult.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
version: row.version,
|
||||
platform_id: row.platform_id,
|
||||
category_id: row.category_id,
|
||||
license_id: row.license_id,
|
||||
type: row.type,
|
||||
repository: row.repository,
|
||||
homepage_url: row.homepage_url,
|
||||
download_url: row.download_url,
|
||||
last_updated: row.last_updated,
|
||||
downloads_count: row.downloads_count,
|
||||
popularity_score: row.popularity_score,
|
||||
is_active: row.is_active,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
platform: row.platform_name ? {
|
||||
id: row.platform_id,
|
||||
name: row.platform_name,
|
||||
package_manager: row.platform_package_manager,
|
||||
icon: row.platform_icon
|
||||
} : undefined,
|
||||
category: row.category_name ? {
|
||||
id: row.category_id,
|
||||
name: row.category_name
|
||||
} : undefined,
|
||||
license: row.license_name ? {
|
||||
id: row.license_id,
|
||||
name: row.license_name,
|
||||
url: row.license_url
|
||||
} : undefined,
|
||||
tags: row.tags || []
|
||||
}))
|
||||
|
||||
return { packages, total }
|
||||
}
|
||||
|
||||
// Get package by ID
|
||||
static async getById(id: string): Promise<Package | null> {
|
||||
const packageQuery = `
|
||||
SELECT
|
||||
p.*,
|
||||
pl.name as platform_name,
|
||||
pl.package_manager as platform_package_manager,
|
||||
pl.icon as platform_icon,
|
||||
c.name as category_name,
|
||||
l.name as license_name,
|
||||
l.url as license_url,
|
||||
COALESCE(
|
||||
ARRAY_AGG(DISTINCT pt.tag) FILTER (WHERE pt.tag IS NOT NULL),
|
||||
ARRAY[]::VARCHAR[]
|
||||
) as tags
|
||||
FROM packages p
|
||||
LEFT JOIN platforms pl ON p.platform_id = pl.id
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
LEFT JOIN licenses l ON p.license_id = l.id
|
||||
LEFT JOIN package_tags pt ON p.id = pt.package_id
|
||||
WHERE p.id = $1 AND p.is_active = true
|
||||
GROUP BY p.id, pl.name, pl.package_manager, pl.icon, c.name, l.name, l.url
|
||||
`
|
||||
|
||||
const result = await query(packageQuery, [id])
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const row = result.rows[0]
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
version: row.version,
|
||||
platform_id: row.platform_id,
|
||||
category_id: row.category_id,
|
||||
license_id: row.license_id,
|
||||
type: row.type,
|
||||
repository: row.repository,
|
||||
homepage_url: row.homepage_url,
|
||||
download_url: row.download_url,
|
||||
last_updated: row.last_updated,
|
||||
downloads_count: row.downloads_count,
|
||||
popularity_score: row.popularity_score,
|
||||
is_active: row.is_active,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
platform: row.platform_name ? {
|
||||
id: row.platform_id,
|
||||
name: row.platform_name,
|
||||
package_manager: row.platform_package_manager,
|
||||
icon: row.platform_icon
|
||||
} : undefined,
|
||||
category: row.category_name ? {
|
||||
id: row.category_id,
|
||||
name: row.category_name
|
||||
} : undefined,
|
||||
license: row.license_name ? {
|
||||
id: row.license_id,
|
||||
name: row.license_name,
|
||||
url: row.license_url
|
||||
} : undefined,
|
||||
tags: row.tags || []
|
||||
}
|
||||
}
|
||||
|
||||
// Create new package
|
||||
static async create(data: CreatePackageInput): Promise<Package> {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
platform_id,
|
||||
category_id,
|
||||
license_id,
|
||||
type,
|
||||
repository,
|
||||
homepage_url,
|
||||
download_url,
|
||||
popularity_score
|
||||
} = data
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO packages (
|
||||
id, name, description, version, platform_id, category_id,
|
||||
license_id, type, repository, homepage_url, download_url,
|
||||
popularity_score
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING *`,
|
||||
[id, name, description, version, platform_id, category_id,
|
||||
license_id, type, repository, homepage_url, download_url,
|
||||
popularity_score]
|
||||
)
|
||||
|
||||
const createdPackage = await this.getById(result.rows[0].id)
|
||||
if (!createdPackage) {
|
||||
throw new Error('Failed to create package')
|
||||
}
|
||||
return createdPackage
|
||||
}
|
||||
|
||||
// Update package
|
||||
static async update(id: string, data: UpdatePackageInput): Promise<Package | null> {
|
||||
const fields = []
|
||||
const values = []
|
||||
let paramIndex = 1
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value !== undefined) {
|
||||
fields.push(`${key} = $${paramIndex++}`)
|
||||
values.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return this.getById(id)
|
||||
}
|
||||
|
||||
values.push(id)
|
||||
await query(
|
||||
`UPDATE packages SET ${fields.join(', ')} WHERE id = $${paramIndex}`,
|
||||
values
|
||||
)
|
||||
|
||||
return this.getById(id)
|
||||
}
|
||||
|
||||
// Delete package (soft delete)
|
||||
static async delete(id: string): Promise<boolean> {
|
||||
const result = await query(
|
||||
'UPDATE packages SET is_active = false WHERE id = $1',
|
||||
[id]
|
||||
)
|
||||
return (result.rowCount ?? 0) > 0
|
||||
}
|
||||
|
||||
// Add tags to package
|
||||
static async addTags(packageId: string, tags: string[]): Promise<void> {
|
||||
if (tags.length === 0) return
|
||||
|
||||
const values = tags.map((tag, index) => `($1, $${index + 2})`).join(', ')
|
||||
const params = [packageId, ...tags]
|
||||
|
||||
await query(
|
||||
`INSERT INTO package_tags (package_id, tag) VALUES ${values} ON CONFLICT DO NOTHING`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
// Remove tags from package
|
||||
static async removeTags(packageId: string, tags: string[]): Promise<void> {
|
||||
if (tags.length === 0) return
|
||||
|
||||
await query(
|
||||
'DELETE FROM package_tags WHERE package_id = $1 AND tag = ANY($2)',
|
||||
[packageId, tags]
|
||||
)
|
||||
}
|
||||
|
||||
// Get popular packages
|
||||
static async getPopular(limit: number = 10): Promise<Package[]> {
|
||||
const result = await this.getMany({
|
||||
limit,
|
||||
sort_by: 'popularity_score',
|
||||
sort_order: 'desc'
|
||||
})
|
||||
return result.packages
|
||||
}
|
||||
|
||||
// Get recently updated packages
|
||||
static async getRecentlyUpdated(limit: number = 10): Promise<Package[]> {
|
||||
const result = await this.getMany({
|
||||
limit,
|
||||
sort_by: 'last_updated',
|
||||
sort_order: 'desc'
|
||||
})
|
||||
return result.packages
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { query } from '@/lib/database/config'
|
||||
import { Platform, CreatePlatformInput, UpdatePlatformInput } from '@/models/Platform'
|
||||
|
||||
export class PlatformService {
|
||||
// Get all platforms
|
||||
static async getAll(): Promise<Platform[]> {
|
||||
const result = await query('SELECT * FROM platforms ORDER BY name')
|
||||
return result.rows
|
||||
}
|
||||
|
||||
// Get platform by ID
|
||||
static async getById(id: string): Promise<Platform | null> {
|
||||
const result = await query('SELECT * FROM platforms WHERE id = $1', [id])
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
// Create new platform
|
||||
static async create(data: CreatePlatformInput): Promise<Platform> {
|
||||
const { id, name, package_manager, icon } = data
|
||||
const result = await query(
|
||||
`INSERT INTO platforms (id, name, package_manager, icon)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *`,
|
||||
[id, name, package_manager, icon]
|
||||
)
|
||||
return result.rows[0]
|
||||
}
|
||||
|
||||
// Update platform
|
||||
static async update(id: string, data: UpdatePlatformInput): Promise<Platform | null> {
|
||||
const fields = []
|
||||
const values = []
|
||||
let paramIndex = 1
|
||||
|
||||
if (data.name !== undefined) {
|
||||
fields.push(`name = $${paramIndex++}`)
|
||||
values.push(data.name)
|
||||
}
|
||||
if (data.package_manager !== undefined) {
|
||||
fields.push(`package_manager = $${paramIndex++}`)
|
||||
values.push(data.package_manager)
|
||||
}
|
||||
if (data.icon !== undefined) {
|
||||
fields.push(`icon = $${paramIndex++}`)
|
||||
values.push(data.icon)
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return this.getById(id)
|
||||
}
|
||||
|
||||
values.push(id)
|
||||
const result = await query(
|
||||
`UPDATE platforms SET ${fields.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
||||
values
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
// Delete platform
|
||||
static async delete(id: string): Promise<boolean> {
|
||||
const result = await query('DELETE FROM platforms WHERE id = $1', [id])
|
||||
return (result.rowCount ?? 0) > 0
|
||||
}
|
||||
|
||||
// Check if platform exists
|
||||
static async exists(id: string): Promise<boolean> {
|
||||
const result = await query('SELECT 1 FROM platforms WHERE id = $1 LIMIT 1', [id])
|
||||
return result.rows.length > 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user