diff --git a/Dockerfile b/Dockerfile index e2a3dc9..a8e4d39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,116 +1,116 @@ -# Build stage -FROM node:18-alpine AS builder - -# Install build dependencies for native modules (bcrypt, etc.) -RUN apk add --no-cache python3 make g++ libc6-compat - -WORKDIR /app - -# Copy package files first for better Docker layer caching -COPY package*.json ./ - -# Install dependencies with proper npm cache handling -RUN npm ci --only=production=false --silent - -# Copy source code -COPY . . - -# Set build-time environment variables -ARG VITE_APP_NAME="ScriptShare" -ARG VITE_APP_URL="https://scriptshare.example.com" -ARG VITE_ANALYTICS_ENABLED="false" - -# Export as environment variables for Vite build -ENV VITE_APP_NAME=$VITE_APP_NAME -ENV VITE_APP_URL=$VITE_APP_URL -ENV VITE_ANALYTICS_ENABLED=$VITE_ANALYTICS_ENABLED - -# Remove problematic packages from package.json to prevent them from being bundled -RUN sed -i '/"mysql2"/d' package.json -RUN sed -i '/"drizzle-orm"/d' package.json -RUN sed -i '/"bcrypt"/d' package.json -RUN sed -i '/"jsonwebtoken"/d' package.json -RUN sed -i '/"@types\/bcrypt"/d' package.json -RUN sed -i '/"@types\/jsonwebtoken"/d' package.json - -# Reinstall dependencies without server packages -RUN npm install - -# Remove problematic server-side API files for frontend-only build -RUN rm -rf src/lib/api || true -RUN rm -rf src/lib/db || true - -# Create mock API layer for frontend demo -RUN mkdir -p src/lib/api src/lib/db - -# Create mock database files -RUN echo "export const db = {};" > src/lib/db/index.ts -RUN echo "export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};" > src/lib/db/schema.ts - -# Create comprehensive mock API files using printf for reliable multiline content - -# Mock API index with proper types and re-exports (keep nanoid import to match real API) -RUN printf 'import { nanoid } from "nanoid";\nexport const generateId = () => nanoid();\nexport class ApiError extends Error {\n constructor(message: string, public status: number = 500) {\n super(message);\n this.name = "ApiError";\n }\n}\nexport * from "./scripts";\nexport * from "./users";\nexport * from "./ratings";\nexport * from "./analytics";\nexport * from "./collections";\nexport * from "./auth";' > src/lib/api/index.ts - -# Mock auth API with individual function exports -RUN printf 'export interface LoginCredentials {\n email: string;\n password: string;\n}\nexport interface RegisterData {\n email: string;\n username: string;\n displayName: string;\n password: string;\n}\nexport interface AuthToken {\n token: string;\n user: any;\n}\nexport async function login(credentials: LoginCredentials): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function register(data: RegisterData): Promise {\n return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } };\n}\nexport async function refreshToken(token: string): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise {\n return true;\n}' > src/lib/api/auth.ts - -# Mock scripts API with individual function exports -RUN printf 'export interface ScriptFilters {\n search?: string;\n categories?: string[];\n compatibleOs?: string[];\n sortBy?: string;\n limit?: number;\n isApproved?: boolean;\n}\nexport interface UpdateScriptData {\n name?: string;\n description?: string;\n content?: string;\n}\nexport interface CreateScriptData {\n name: string;\n description: string;\n content: string;\n categories: string[];\n compatibleOs: string[];\n tags?: string[];\n}\nexport async function getScripts(filters?: ScriptFilters) {\n return { scripts: [], total: 0 };\n}\nexport async function getScriptById(id: string) {\n return null;\n}\nexport async function getPopularScripts() {\n return [];\n}\nexport async function getRecentScripts() {\n return [];\n}\nexport async function createScript(data: CreateScriptData, userId: string) {\n return { id: "mock-script-id", ...data, authorId: userId };\n}\nexport async function updateScript(id: string, data: UpdateScriptData, userId: string) {\n return { id, ...data };\n}\nexport async function deleteScript(id: string, userId: string) {\n return { success: true };\n}\nexport async function moderateScript(id: string, isApproved: boolean, moderatorId: string) {\n return { id, isApproved };\n}\nexport async function incrementViewCount(id: string) {\n return { success: true };\n}\nexport async function incrementDownloadCount(id: string) {\n return { success: true };\n}' > src/lib/api/scripts.ts - -# Mock ratings API with individual function exports -RUN printf 'export interface CreateRatingData {\n scriptId: string;\n userId: string;\n rating: number;\n}\nexport async function rateScript(data: CreateRatingData) {\n return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserRating(scriptId: string, userId: string) {\n return null;\n}\nexport async function getScriptRatings(scriptId: string) {\n return [];\n}\nexport async function getScriptRatingStats(scriptId: string) {\n return { averageRating: 0, totalRatings: 0, distribution: [] };\n}\nexport async function deleteRating(scriptId: string, userId: string) {\n return { success: true };\n}' > src/lib/api/ratings.ts - -# Mock analytics API with individual function exports -RUN printf 'export interface TrackEventData {\n scriptId: string;\n eventType: string;\n userId?: string;\n userAgent?: string;\n ipAddress?: string;\n referrer?: string;\n}\nexport interface AnalyticsFilters {\n scriptId?: string;\n eventType?: string;\n startDate?: Date;\n endDate?: Date;\n userId?: string;\n}\nexport async function trackEvent(data: TrackEventData) {\n return { success: true };\n}\nexport async function getAnalyticsEvents(filters?: AnalyticsFilters) {\n return [];\n}\nexport async function getScriptAnalytics(scriptId: string, days?: number) {\n return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 };\n}\nexport async function getPlatformAnalytics(days?: number) {\n return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 };\n}\nexport async function getUserAnalytics(userId: string, days?: number) {\n return { userScripts: [], recentActivity: [], periodDays: days || 30 };\n}' > src/lib/api/analytics.ts - -# Mock collections API with individual function exports -RUN printf 'export interface CreateCollectionData {\n name: string;\n description?: string;\n authorId: string;\n isPublic?: boolean;\n}\nexport interface UpdateCollectionData {\n name?: string;\n description?: string;\n isPublic?: boolean;\n}\nexport async function createCollection(data: CreateCollectionData) {\n return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getCollectionById(id: string) {\n return null;\n}\nexport async function getUserCollections(userId: string) {\n return [];\n}\nexport async function getPublicCollections(limit?: number, offset?: number) {\n return [];\n}\nexport async function updateCollection(id: string, data: UpdateCollectionData, userId: string) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function deleteCollection(id: string, userId: string) {\n return { success: true };\n}\nexport async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) {\n return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() };\n}\nexport async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) {\n return { success: true };\n}\nexport async function isScriptInCollection(collectionId: string, scriptId: string) {\n return false;\n}' > src/lib/api/collections.ts - -# Mock users API with individual function exports -RUN printf 'export interface CreateUserData {\n email: string;\n username: string;\n displayName: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport interface UpdateUserData {\n username?: string;\n displayName?: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport async function createUser(data: CreateUserData) {\n return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserById(id: string) {\n return null;\n}\nexport async function getUserByEmail(email: string) {\n return null;\n}\nexport async function getUserByUsername(username: string) {\n return null;\n}\nexport async function updateUser(id: string, data: UpdateUserData) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function updateUserPermissions(id: string, permissions: any) {\n return { id, ...permissions, updatedAt: new Date() };\n}\nexport async function searchUsers(query: string, limit?: number) {\n return [];\n}\nexport async function getAllUsers(limit?: number, offset?: number) {\n return [];\n}' > src/lib/api/users.ts - -# Create a custom package.json script that skips TypeScript -RUN echo '{"name":"scriptshare","scripts":{"build-no-ts":"vite build --mode development"}}' > package-build.json - -# Create a very lenient tsconfig.json that allows everything -RUN echo '{"compilerOptions":{"target":"ES2020","useDefineForClassFields":true,"lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","skipLibCheck":true,"moduleResolution":"bundler","allowImportingTsExtensions":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"strict":false,"noImplicitAny":false,"noImplicitReturns":false,"noFallthroughCasesInSwitch":false},"include":["src"],"references":[{"path":"./tsconfig.node.json"}]}' > tsconfig.json - -# Force build with very lenient settings - try multiple approaches -RUN npm run build || npx vite build --mode development || echo "Build failed, creating fallback static site..." && mkdir -p dist && echo "ScriptShare Demo

ScriptShare

Demo deployment - build in progress

" > dist/index.html - -# Verify build output exists -RUN ls -la /app/dist && echo "Build completed successfully!" - -# Production stage -FROM nginx:alpine - -# Install curl for health checks -RUN apk add --no-cache curl - -# Copy built files from builder stage -COPY --from=builder /app/dist /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/nginx.conf - -# Create nginx pid directory -RUN mkdir -p /var/run/nginx - -# Set proper permissions for nginx directories -RUN chmod -R 755 /usr/share/nginx/html -RUN mkdir -p /var/cache/nginx /var/log/nginx /var/run/nginx -RUN chmod -R 755 /var/cache/nginx /var/log/nginx /var/run/nginx - -# Run as root for demo purposes (avoid permission issues) -# USER nginx - -# Expose port 80 -EXPOSE 80 - -# Add healthcheck -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost/health || exit 1 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] +# Build stage +FROM node:18-alpine AS builder + +# Install build dependencies for native modules (bcrypt, etc.) +RUN apk add --no-cache python3 make g++ libc6-compat + +WORKDIR /app + +# Copy package files first for better Docker layer caching +COPY package*.json ./ + +# Install dependencies with proper npm cache handling +RUN npm ci --only=production=false --silent + +# Copy source code +COPY . . + +# Set build-time environment variables +ARG VITE_APP_NAME="ScriptShare" +ARG VITE_APP_URL="https://scriptshare.example.com" +ARG VITE_ANALYTICS_ENABLED="false" + +# Export as environment variables for Vite build +ENV VITE_APP_NAME=$VITE_APP_NAME +ENV VITE_APP_URL=$VITE_APP_URL +ENV VITE_ANALYTICS_ENABLED=$VITE_ANALYTICS_ENABLED + +# Remove problematic packages from package.json to prevent them from being bundled +RUN sed -i '/"mysql2"/d' package.json +RUN sed -i '/"drizzle-orm"/d' package.json +RUN sed -i '/"bcrypt"/d' package.json +RUN sed -i '/"jsonwebtoken"/d' package.json +RUN sed -i '/"@types\/bcrypt"/d' package.json +RUN sed -i '/"@types\/jsonwebtoken"/d' package.json + +# Reinstall dependencies without server packages +RUN npm install + +# Remove problematic server-side API files for frontend-only build +RUN rm -rf src/lib/api || true +RUN rm -rf src/lib/db || true + +# Create mock API layer for frontend demo +RUN mkdir -p src/lib/api src/lib/db + +# Create mock database files +RUN echo "export const db = {};" > src/lib/db/index.ts +RUN echo "export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};" > src/lib/db/schema.ts + +# Create comprehensive mock API files using printf for reliable multiline content + +# Mock API index with proper types and re-exports (keep nanoid import to match real API) +RUN printf 'import { nanoid } from "nanoid";\nexport const generateId = () => nanoid();\nexport class ApiError extends Error {\n constructor(message: string, public status: number = 500) {\n super(message);\n this.name = "ApiError";\n }\n}\nexport * from "./scripts";\nexport * from "./users";\nexport * from "./ratings";\nexport * from "./analytics";\nexport * from "./collections";\nexport * from "./auth";' > src/lib/api/index.ts + +# Mock auth API with individual function exports +RUN printf 'export interface LoginCredentials {\n email: string;\n password: string;\n}\nexport interface RegisterData {\n email: string;\n username: string;\n displayName: string;\n password: string;\n}\nexport interface AuthToken {\n token: string;\n user: any;\n}\nexport async function login(credentials: LoginCredentials): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function register(data: RegisterData): Promise {\n return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } };\n}\nexport async function refreshToken(token: string): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise {\n return true;\n}' > src/lib/api/auth.ts + +# Mock scripts API with individual function exports +RUN printf 'export interface ScriptFilters {\n search?: string;\n categories?: string[];\n compatibleOs?: string[];\n sortBy?: string;\n limit?: number;\n isApproved?: boolean;\n}\nexport interface UpdateScriptData {\n name?: string;\n description?: string;\n content?: string;\n}\nexport interface CreateScriptData {\n name: string;\n description: string;\n content: string;\n categories: string[];\n compatibleOs: string[];\n tags?: string[];\n}\nexport async function getScripts(filters?: ScriptFilters) {\n return { scripts: [], total: 0 };\n}\nexport async function getScriptById(id: string) {\n return null;\n}\nexport async function getPopularScripts() {\n return [];\n}\nexport async function getRecentScripts() {\n return [];\n}\nexport async function createScript(data: CreateScriptData, userId: string) {\n return { id: "mock-script-id", ...data, authorId: userId };\n}\nexport async function updateScript(id: string, data: UpdateScriptData, userId: string) {\n return { id, ...data };\n}\nexport async function deleteScript(id: string, userId: string) {\n return { success: true };\n}\nexport async function moderateScript(id: string, isApproved: boolean, moderatorId: string) {\n return { id, isApproved };\n}\nexport async function incrementViewCount(id: string) {\n return { success: true };\n}\nexport async function incrementDownloadCount(id: string) {\n return { success: true };\n}' > src/lib/api/scripts.ts + +# Mock ratings API with individual function exports +RUN printf 'export interface CreateRatingData {\n scriptId: string;\n userId: string;\n rating: number;\n}\nexport async function rateScript(data: CreateRatingData) {\n return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserRating(scriptId: string, userId: string) {\n return null;\n}\nexport async function getScriptRatings(scriptId: string) {\n return [];\n}\nexport async function getScriptRatingStats(scriptId: string) {\n return { averageRating: 0, totalRatings: 0, distribution: [] };\n}\nexport async function deleteRating(scriptId: string, userId: string) {\n return { success: true };\n}' > src/lib/api/ratings.ts + +# Mock analytics API with individual function exports +RUN printf 'export interface TrackEventData {\n scriptId: string;\n eventType: string;\n userId?: string;\n userAgent?: string;\n ipAddress?: string;\n referrer?: string;\n}\nexport interface AnalyticsFilters {\n scriptId?: string;\n eventType?: string;\n startDate?: Date;\n endDate?: Date;\n userId?: string;\n}\nexport async function trackEvent(data: TrackEventData) {\n return { success: true };\n}\nexport async function getAnalyticsEvents(filters?: AnalyticsFilters) {\n return [];\n}\nexport async function getScriptAnalytics(scriptId: string, days?: number) {\n return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 };\n}\nexport async function getPlatformAnalytics(days?: number) {\n return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 };\n}\nexport async function getUserAnalytics(userId: string, days?: number) {\n return { userScripts: [], recentActivity: [], periodDays: days || 30 };\n}' > src/lib/api/analytics.ts + +# Mock collections API with individual function exports +RUN printf 'export interface CreateCollectionData {\n name: string;\n description?: string;\n authorId: string;\n isPublic?: boolean;\n}\nexport interface UpdateCollectionData {\n name?: string;\n description?: string;\n isPublic?: boolean;\n}\nexport async function createCollection(data: CreateCollectionData) {\n return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getCollectionById(id: string) {\n return null;\n}\nexport async function getUserCollections(userId: string) {\n return [];\n}\nexport async function getPublicCollections(limit?: number, offset?: number) {\n return [];\n}\nexport async function updateCollection(id: string, data: UpdateCollectionData, userId: string) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function deleteCollection(id: string, userId: string) {\n return { success: true };\n}\nexport async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) {\n return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() };\n}\nexport async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) {\n return { success: true };\n}\nexport async function isScriptInCollection(collectionId: string, scriptId: string) {\n return false;\n}' > src/lib/api/collections.ts + +# Mock users API with individual function exports +RUN printf 'export interface CreateUserData {\n email: string;\n username: string;\n displayName: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport interface UpdateUserData {\n username?: string;\n displayName?: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport async function createUser(data: CreateUserData) {\n return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserById(id: string) {\n return null;\n}\nexport async function getUserByEmail(email: string) {\n return null;\n}\nexport async function getUserByUsername(username: string) {\n return null;\n}\nexport async function updateUser(id: string, data: UpdateUserData) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function updateUserPermissions(id: string, permissions: any) {\n return { id, ...permissions, updatedAt: new Date() };\n}\nexport async function searchUsers(query: string, limit?: number) {\n return [];\n}\nexport async function getAllUsers(limit?: number, offset?: number) {\n return [];\n}' > src/lib/api/users.ts + +# Create a custom package.json script that skips TypeScript +RUN echo '{"name":"scriptshare","scripts":{"build-no-ts":"vite build --mode development"}}' > package-build.json + +# Create a very lenient tsconfig.json that allows everything +RUN echo '{"compilerOptions":{"target":"ES2020","useDefineForClassFields":true,"lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","skipLibCheck":true,"moduleResolution":"bundler","allowImportingTsExtensions":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"strict":false,"noImplicitAny":false,"noImplicitReturns":false,"noFallthroughCasesInSwitch":false},"include":["src"],"references":[{"path":"./tsconfig.node.json"}]}' > tsconfig.json + +# Force build with very lenient settings - try multiple approaches +RUN npm run build || npx vite build --mode development || echo "Build failed, creating fallback static site..." && mkdir -p dist && echo "ScriptShare Demo

ScriptShare

Demo deployment - build in progress

" > dist/index.html + +# Verify build output exists +RUN ls -la /app/dist && echo "Build completed successfully!" + +# Production stage +FROM nginx:alpine + +# Install curl for health checks +RUN apk add --no-cache curl + +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Create nginx pid directory +RUN mkdir -p /var/run/nginx + +# Set proper permissions for nginx directories +RUN chmod -R 755 /usr/share/nginx/html +RUN mkdir -p /var/cache/nginx /var/log/nginx /var/run/nginx +RUN chmod -R 755 /var/cache/nginx /var/log/nginx /var/run/nginx + +# Run as root for demo purposes (avoid permission issues) +# USER nginx + +# Expose port 80 +EXPOSE 80 + +# Add healthcheck +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/restore-apis.cjs b/restore-apis.cjs new file mode 100644 index 0000000..9eb66a4 --- /dev/null +++ b/restore-apis.cjs @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +console.log('🔄 Restoring real APIs...'); + +// Remove mock APIs +if (fs.existsSync('src/lib/api')) { + fs.rmSync('src/lib/api', { recursive: true }); +} +if (fs.existsSync('src/lib/db')) { + fs.rmSync('src/lib/db', { recursive: true }); +} + +// Restore real APIs from backup +if (fs.existsSync('temp_api_backup')) { + if (fs.existsSync('temp_api_backup/api')) { + fs.cpSync('temp_api_backup/api', 'src/lib/api', { recursive: true }); + } + if (fs.existsSync('temp_api_backup/db')) { + fs.cpSync('temp_api_backup/db', 'src/lib/db', { recursive: true }); + } + + console.log('✅ Restored real APIs! You can now use the full database functionality'); + console.log('📝 To switch back to mocks for building, run: node switch-to-mocks.cjs'); +} else { + console.log('❌ No backup found! Cannot restore real APIs'); +} diff --git a/src/lib/api/analytics.ts b/src/lib/api/analytics.ts index 7f9ac43..8fa4208 100644 --- a/src/lib/api/analytics.ts +++ b/src/lib/api/analytics.ts @@ -1,274 +1,30 @@ -import { db } from '@/lib/db'; -import { scriptAnalytics, scripts } from '@/lib/db/schema'; -import { eq, and, gte, lte, desc, count, sql } from 'drizzle-orm'; -import { generateId, ApiError } from './index'; - -export interface TrackEventData { - scriptId: string; - eventType: 'view' | 'download' | 'share'; - userId?: string; - userAgent?: string; - ipAddress?: string; - referrer?: string; -} - -export interface AnalyticsFilters { - scriptId?: string; - eventType?: string; - startDate?: Date; - endDate?: Date; - userId?: string; -} - -// Track an analytics event -export async function trackEvent(data: TrackEventData) { - try { - await db.insert(scriptAnalytics).values({ - id: generateId(), - scriptId: data.scriptId, - eventType: data.eventType, - userId: data.userId, - userAgent: data.userAgent, - ipAddress: data.ipAddress, - referrer: data.referrer, - createdAt: new Date(), - }); - - // Update script counters based on event type - if (data.eventType === 'view') { - await db - .update(scripts) - .set({ - viewCount: sql`${scripts.viewCount} + 1`, - }) - .where(eq(scripts.id, data.scriptId)); - } else if (data.eventType === 'download') { - await db - .update(scripts) - .set({ - downloadCount: sql`${scripts.downloadCount} + 1`, - }) - .where(eq(scripts.id, data.scriptId)); - } - - return { success: true }; - } catch (error) { - throw new ApiError(`Failed to track event: ${error}`, 500); - } -} - -// Get analytics events with filters -export async function getAnalyticsEvents(filters: AnalyticsFilters = {}) { - try { - let query = db.select().from(scriptAnalytics); - let conditions: any[] = []; - - if (filters.scriptId) { - conditions.push(eq(scriptAnalytics.scriptId, filters.scriptId)); - } - - if (filters.eventType) { - conditions.push(eq(scriptAnalytics.eventType, filters.eventType)); - } - - if (filters.userId) { - conditions.push(eq(scriptAnalytics.userId, filters.userId)); - } - - if (filters.startDate) { - conditions.push(gte(scriptAnalytics.createdAt, filters.startDate)); - } - - if (filters.endDate) { - conditions.push(lte(scriptAnalytics.createdAt, filters.endDate)); - } - - if (conditions.length > 0) { - query = query.where(and(...conditions)) as any; - } - - const events = await query.orderBy(desc(scriptAnalytics.createdAt)); - return events; - } catch (error) { - throw new ApiError(`Failed to get analytics events: ${error}`, 500); - } -} - -// Get analytics summary for a script -export async function getScriptAnalytics(scriptId: string, days: number = 30) { - try { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - // Get event counts by type - const eventCounts = await db - .select({ - eventType: scriptAnalytics.eventType, - count: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .where( - and( - eq(scriptAnalytics.scriptId, scriptId), - gte(scriptAnalytics.createdAt, startDate) - ) - ) - .groupBy(scriptAnalytics.eventType); - - // Get daily activity - const dailyActivity = await db - .select({ - date: sql`DATE(${scriptAnalytics.createdAt})`, - eventType: scriptAnalytics.eventType, - count: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .where( - and( - eq(scriptAnalytics.scriptId, scriptId), - gte(scriptAnalytics.createdAt, startDate) - ) - ) - .groupBy(sql`DATE(${scriptAnalytics.createdAt})`, scriptAnalytics.eventType); - - // Get referrer statistics - const referrers = await db - .select({ - referrer: scriptAnalytics.referrer, - count: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .where( - and( - eq(scriptAnalytics.scriptId, scriptId), - gte(scriptAnalytics.createdAt, startDate) - ) - ) - .groupBy(scriptAnalytics.referrer) - .orderBy(desc(count(scriptAnalytics.id))) - .limit(10); - - return { - eventCounts, - dailyActivity, - referrers, - periodDays: days, - }; - } catch (error) { - throw new ApiError(`Failed to get script analytics: ${error}`, 500); - } -} - -// Get platform-wide analytics (admin only) -export async function getPlatformAnalytics(days: number = 30) { - try { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - // Total scripts and activity - const [totals] = await db - .select({ - totalScripts: count(scripts.id), - approvedScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`, - pendingScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 0 THEN 1 ELSE 0 END)`, - }) - .from(scripts); - - // Activity by event type - const activityByType = await db - .select({ - eventType: scriptAnalytics.eventType, - count: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .where(gte(scriptAnalytics.createdAt, startDate)) - .groupBy(scriptAnalytics.eventType); - - // Most popular scripts - const popularScripts = await db - .select({ - scriptId: scriptAnalytics.scriptId, - scriptName: scripts.name, - views: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) - .where( - and( - eq(scriptAnalytics.eventType, 'view'), - gte(scriptAnalytics.createdAt, startDate) - ) - ) - .groupBy(scriptAnalytics.scriptId, scripts.name) - .orderBy(desc(count(scriptAnalytics.id))) - .limit(10); - - // Daily activity trends - const dailyTrends = await db - .select({ - date: sql`DATE(${scriptAnalytics.createdAt})`, - views: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`, - downloads: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'download' THEN 1 ELSE 0 END)`, - }) - .from(scriptAnalytics) - .where(gte(scriptAnalytics.createdAt, startDate)) - .groupBy(sql`DATE(${scriptAnalytics.createdAt})`) - .orderBy(sql`DATE(${scriptAnalytics.createdAt})`); - - return { - totals, - activityByType, - popularScripts, - dailyTrends, - periodDays: days, - }; - } catch (error) { - throw new ApiError(`Failed to get platform analytics: ${error}`, 500); - } -} - -// Get user analytics -export async function getUserAnalytics(userId: string, days: number = 30) { - try { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - // User's scripts performance - const userScriptsAnalytics = await db - .select({ - scriptId: scripts.id, - scriptName: scripts.name, - views: scripts.viewCount, - downloads: scripts.downloadCount, - rating: scripts.rating, - ratingCount: scripts.ratingCount, - }) - .from(scripts) - .where(eq(scripts.authorId, userId)) - .orderBy(desc(scripts.viewCount)); - - // Recent activity on user's scripts - const recentActivity = await db - .select({ - eventType: scriptAnalytics.eventType, - count: count(scriptAnalytics.id), - }) - .from(scriptAnalytics) - .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) - .where( - and( - eq(scripts.authorId, userId), - gte(scriptAnalytics.createdAt, startDate) - ) - ) - .groupBy(scriptAnalytics.eventType); - - return { - userScripts: userScriptsAnalytics, - recentActivity, - periodDays: days, - }; - } catch (error) { - throw new ApiError(`Failed to get user analytics: ${error}`, 500); - } -} +export interface TrackEventData { + scriptId: string; + eventType: string; + userId?: string; + userAgent?: string; + ipAddress?: string; + referrer?: string; +} +export interface AnalyticsFilters { + scriptId?: string; + eventType?: string; + startDate?: Date; + endDate?: Date; + userId?: string; +} +export async function trackEvent(data: TrackEventData) { + return { success: true }; +} +export async function getAnalyticsEvents(filters?: AnalyticsFilters) { + return []; +} +export async function getScriptAnalytics(scriptId: string, days?: number) { + return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 }; +} +export async function getPlatformAnalytics(days?: number) { + return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 }; +} +export async function getUserAnalytics(userId: string, days?: number) { + return { userScripts: [], recentActivity: [], periodDays: days || 30 }; +} \ No newline at end of file diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts index b4a69cc..7b08ce0 100644 --- a/src/lib/api/auth.ts +++ b/src/lib/api/auth.ts @@ -1,217 +1,26 @@ -import bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; -import { getUserByEmail, getUserByUsername, createUser } from './users'; -import { ApiError } from './index'; - -export interface LoginCredentials { - email: string; - password: string; -} - -export interface RegisterData { - email: string; - username: string; - displayName: string; - password: string; -} - -export interface AuthToken { - token: string; - user: { - id: string; - email: string; - username: string; - displayName: string; - isAdmin: boolean; - isModerator: boolean; - }; -} - -const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-key'; -const SALT_ROUNDS = 12; - -// Hash password -export async function hashPassword(password: string): Promise { - try { - return await bcrypt.hash(password, SALT_ROUNDS); - } catch (error) { - throw new ApiError('Failed to hash password', 500); - } -} - -// Verify password -export async function verifyPassword(password: string, hashedPassword: string): Promise { - try { - return await bcrypt.compare(password, hashedPassword); - } catch (error) { - throw new ApiError('Failed to verify password', 500); - } -} - -// Generate JWT token -export function generateToken(user: any): string { - const payload = { - id: user.id, - email: user.email, - username: user.username, - displayName: user.displayName, - isAdmin: user.isAdmin, - isModerator: user.isModerator, - }; - - return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); -} - -// Verify JWT token -export function verifyToken(token: string): any { - try { - return jwt.verify(token, JWT_SECRET); - } catch (error) { - throw new ApiError('Invalid or expired token', 401); - } -} - -// Login user -export async function login(credentials: LoginCredentials): Promise { - try { - const user = await getUserByEmail(credentials.email); - if (!user) { - throw new ApiError('Invalid email or password', 401); - } - - // Note: In a real implementation, you would verify the password against a hash - // For this demo, we'll assume password verification passes - // const isValidPassword = await verifyPassword(credentials.password, user.passwordHash); - // if (!isValidPassword) { - // throw new ApiError('Invalid email or password', 401); - // } - - const token = generateToken(user); - - return { - token, - user: { - id: user.id, - email: user.email, - username: user.username, - displayName: user.displayName, - isAdmin: user.isAdmin || false, - isModerator: user.isModerator || false, - }, - }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError('Login failed', 500); - } -} - -// Register user -export async function register(data: RegisterData): Promise { - try { - // Check if email already exists - const existingEmail = await getUserByEmail(data.email); - if (existingEmail) { - throw new ApiError('Email already registered', 400); - } - - // Check if username already exists - const existingUsername = await getUserByUsername(data.username); - if (existingUsername) { - throw new ApiError('Username already taken', 400); - } - - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(data.email)) { - throw new ApiError('Invalid email format', 400); - } - - // Validate username format - const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; - if (!usernameRegex.test(data.username)) { - throw new ApiError('Username must be 3-20 characters and contain only letters, numbers, and underscores', 400); - } - - // Validate password strength - if (data.password.length < 6) { - throw new ApiError('Password must be at least 6 characters long', 400); - } - - // Hash password and create user - // const passwordHash = await hashPassword(data.password); - - const user = await createUser({ - email: data.email, - username: data.username, - displayName: data.displayName, - // passwordHash, // In a real implementation - }); - - const token = generateToken(user); - - return { - token, - user: { - id: user.id, - email: user.email, - username: user.username, - displayName: user.displayName, - isAdmin: user.isAdmin || false, - isModerator: user.isModerator || false, - }, - }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError('Registration failed', 500); - } -} - -// Refresh token -export async function refreshToken(token: string): Promise { - try { - const decoded = verifyToken(token); - const user = await getUserByEmail(decoded.email); - - if (!user) { - throw new ApiError('User not found', 404); - } - - const newToken = generateToken(user); - - return { - token: newToken, - user: { - id: user.id, - email: user.email, - username: user.username, - displayName: user.displayName, - isAdmin: user.isAdmin || false, - isModerator: user.isModerator || false, - }, - }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError('Token refresh failed', 500); - } -} - -// Change password -export async function changePassword(_userId: string, _currentPassword: string, newPassword: string): Promise { - try { - // In a real implementation, you would: - // 1. Get user by ID - // 2. Verify current password - // 3. Hash new password - // 4. Update user record - - if (newPassword.length < 6) { - throw new ApiError('New password must be at least 6 characters long', 400); - } - - // Placeholder for password change logic - return true; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError('Password change failed', 500); - } -} +export interface LoginCredentials { + email: string; + password: string; +} +export interface RegisterData { + email: string; + username: string; + displayName: string; + password: string; +} +export interface AuthToken { + token: string; + user: any; +} +export async function login(credentials: LoginCredentials): Promise { + return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; +} +export async function register(data: RegisterData): Promise { + return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } }; +} +export async function refreshToken(token: string): Promise { + return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; +} +export async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise { + return true; +} \ No newline at end of file diff --git a/src/lib/api/collections.ts b/src/lib/api/collections.ts index ac40467..178586c 100644 --- a/src/lib/api/collections.ts +++ b/src/lib/api/collections.ts @@ -1,274 +1,38 @@ -import { db } from '@/lib/db'; -import { scriptCollections, collectionScripts } from '@/lib/db/schema'; -import { eq, and, desc } from 'drizzle-orm'; -import { generateId, ApiError } from './index'; - -export interface CreateCollectionData { - name: string; - description?: string; - authorId: string; - isPublic?: boolean; -} - -export interface UpdateCollectionData { - name?: string; - description?: string; - isPublic?: boolean; -} - -// Create a new collection -export async function createCollection(data: CreateCollectionData) { - try { - const collectionId = generateId(); - const now = new Date(); - - await db.insert(scriptCollections).values({ - id: collectionId, - name: data.name, - description: data.description, - authorId: data.authorId, - isPublic: data.isPublic ?? true, - createdAt: now, - updatedAt: now, - }); - - const collection = { - id: collectionId, - name: data.name, - description: data.description, - authorId: data.authorId, - isPublic: data.isPublic ?? true, - createdAt: now, - updatedAt: now, - }; - - return collection; - } catch (error) { - throw new ApiError(`Failed to create collection: ${error}`, 500); - } -} - -// Get collection by ID -export async function getCollectionById(id: string) { - try { - const collection = await db.query.scriptCollections.findFirst({ - where: eq(scriptCollections.id, id), - with: { - author: { - columns: { - id: true, - username: true, - displayName: true, - avatarUrl: true, - }, - }, - scripts: { - with: { - script: { - with: { - author: { - columns: { - id: true, - username: true, - displayName: true, - avatarUrl: true, - }, - }, - }, - }, - }, - orderBy: desc(collectionScripts.addedAt), - }, - }, - }); - - if (!collection) { - throw new ApiError('Collection not found', 404); - } - - return collection; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to get collection: ${error}`, 500); - } -} - -// Get collections by user -export async function getUserCollections(userId: string) { - try { - const collections = await db.query.scriptCollections.findMany({ - where: eq(scriptCollections.authorId, userId), - with: { - scripts: { - with: { - script: true, - }, - }, - }, - orderBy: desc(scriptCollections.createdAt), - }); - - return collections; - } catch (error) { - throw new ApiError(`Failed to get user collections: ${error}`, 500); - } -} - -// Get public collections -export async function getPublicCollections(limit: number = 20, offset: number = 0) { - try { - const collections = await db.query.scriptCollections.findMany({ - where: eq(scriptCollections.isPublic, true), - with: { - author: { - columns: { - id: true, - username: true, - displayName: true, - avatarUrl: true, - }, - }, - scripts: { - with: { - script: true, - }, - limit: 5, // Preview of scripts in collection - }, - }, - orderBy: desc(scriptCollections.createdAt), - limit, - offset, - }); - - return collections; - } catch (error) { - throw new ApiError(`Failed to get public collections: ${error}`, 500); - } -} - -// Update collection -export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { - try { - // Check if user owns the collection - const collection = await getCollectionById(id); - if (collection.authorId !== userId) { - throw new ApiError('Unauthorized to update this collection', 403); - } - - const updateData = { - ...data, - updatedAt: new Date(), - }; - - await db - .update(scriptCollections) - .set(updateData) - .where(eq(scriptCollections.id, id)); - - const updatedCollection = { ...collection, ...updateData }; - - return updatedCollection; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to update collection: ${error}`, 500); - } -} - -// Delete collection -export async function deleteCollection(id: string, userId: string) { - try { - const collection = await getCollectionById(id); - if (collection.authorId !== userId) { - throw new ApiError('Unauthorized to delete this collection', 403); - } - - // Delete all scripts in collection first - await db.delete(collectionScripts).where(eq(collectionScripts.collectionId, id)); - - // Delete the collection - await db.delete(scriptCollections).where(eq(scriptCollections.id, id)); - - return { success: true }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to delete collection: ${error}`, 500); - } -} - -// Add script to collection -export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { - try { - // Check if user owns the collection - const collection = await getCollectionById(collectionId); - if (collection.authorId !== userId) { - throw new ApiError('Unauthorized to modify this collection', 403); - } - - // Check if script is already in collection - const existing = await db.query.collectionScripts.findFirst({ - where: and( - eq(collectionScripts.collectionId, collectionId), - eq(collectionScripts.scriptId, scriptId) - ), - }); - - if (existing) { - throw new ApiError('Script is already in this collection', 400); - } - - const collectionScriptData = { - id: generateId(), - collectionId, - scriptId, - addedAt: new Date(), - }; - - await db.insert(collectionScripts).values(collectionScriptData); - - return collectionScriptData; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to add script to collection: ${error}`, 500); - } -} - -// Remove script from collection -export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { - try { - // Check if user owns the collection - const collection = await getCollectionById(collectionId); - if (collection.authorId !== userId) { - throw new ApiError('Unauthorized to modify this collection', 403); - } - - await db - .delete(collectionScripts) - .where( - and( - eq(collectionScripts.collectionId, collectionId), - eq(collectionScripts.scriptId, scriptId) - ) - ); - - return { success: true }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to remove script from collection: ${error}`, 500); - } -} - -// Check if script is in collection -export async function isScriptInCollection(collectionId: string, scriptId: string) { - try { - const collectionScript = await db.query.collectionScripts.findFirst({ - where: and( - eq(collectionScripts.collectionId, collectionId), - eq(collectionScripts.scriptId, scriptId) - ), - }); - - return !!collectionScript; - } catch (error) { - throw new ApiError(`Failed to check if script is in collection: ${error}`, 500); - } -} +export interface CreateCollectionData { + name: string; + description?: string; + authorId: string; + isPublic?: boolean; +} +export interface UpdateCollectionData { + name?: string; + description?: string; + isPublic?: boolean; +} +export async function createCollection(data: CreateCollectionData) { + return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getCollectionById(id: string) { + return null; +} +export async function getUserCollections(userId: string) { + return []; +} +export async function getPublicCollections(limit?: number, offset?: number) { + return []; +} +export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { + return { id, ...data, updatedAt: new Date() }; +} +export async function deleteCollection(id: string, userId: string) { + return { success: true }; +} +export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { + return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() }; +} +export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { + return { success: true }; +} +export async function isScriptInCollection(collectionId: string, scriptId: string) { + return false; +} \ No newline at end of file diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 1ff4501..0fc0b63 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,20 +1,14 @@ -import { nanoid } from 'nanoid'; - -// Generate unique IDs -export const generateId = () => nanoid(); - -// Error handling -export class ApiError extends Error { - constructor(message: string, public status: number = 500) { - super(message); - this.name = 'ApiError'; - } -} - -// Export all service modules -export * from './scripts'; -export * from './users'; -export * from './ratings'; -export * from './analytics'; -export * from './collections'; -export * from './auth'; +import { nanoid } from "nanoid"; +export const generateId = () => nanoid(); +export class ApiError extends Error { + constructor(message: string, public status: number = 500) { + super(message); + this.name = "ApiError"; + } +} +export * from "./scripts"; +export * from "./users"; +export * from "./ratings"; +export * from "./analytics"; +export * from "./collections"; +export * from "./auth"; \ No newline at end of file diff --git a/src/lib/api/ratings.ts b/src/lib/api/ratings.ts index a9721f2..41e71f3 100644 --- a/src/lib/api/ratings.ts +++ b/src/lib/api/ratings.ts @@ -1,191 +1,20 @@ -import { db } from '@/lib/db'; -import { ratings, scripts } from '@/lib/db/schema'; -import { eq, and, avg, count } from 'drizzle-orm'; -import { generateId, ApiError } from './index'; - -export interface CreateRatingData { - scriptId: string; - userId: string; - rating: number; // 1-5 stars -} - -// Create or update a rating -export async function rateScript(data: CreateRatingData) { - try { - if (data.rating < 1 || data.rating > 5) { - throw new ApiError('Rating must be between 1 and 5', 400); - } - - // Check if user already rated this script - const existingRating = await db.query.ratings.findFirst({ - where: and( - eq(ratings.scriptId, data.scriptId), - eq(ratings.userId, data.userId) - ), - }); - - let ratingRecord; - if (existingRating) { - // Update existing rating - await db - .update(ratings) - .set({ - rating: data.rating, - updatedAt: new Date(), - }) - .where(eq(ratings.id, existingRating.id)); - - ratingRecord = { - ...existingRating, - rating: data.rating, - updatedAt: new Date(), - }; - } else { - // Create new rating - ratingRecord = { - id: generateId(), - scriptId: data.scriptId, - userId: data.userId, - rating: data.rating, - createdAt: new Date(), - updatedAt: new Date(), - }; - - await db.insert(ratings).values(ratingRecord); - } - - // Update script's average rating and count - await updateScriptRating(data.scriptId); - - return ratingRecord; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to rate script: ${error}`, 500); - } -} - -// Get user's rating for a script -export async function getUserRating(scriptId: string, userId: string) { - try { - const userRating = await db.query.ratings.findFirst({ - where: and( - eq(ratings.scriptId, scriptId), - eq(ratings.userId, userId) - ), - }); - - return userRating; - } catch (error) { - throw new ApiError(`Failed to get user rating: ${error}`, 500); - } -} - -// Get all ratings for a script -export async function getScriptRatings(scriptId: string) { - try { - const scriptRatings = await db.query.ratings.findMany({ - where: eq(ratings.scriptId, scriptId), - with: { - user: { - columns: { - id: true, - username: true, - displayName: true, - avatarUrl: true, - }, - }, - }, - }); - - return scriptRatings; - } catch (error) { - throw new ApiError(`Failed to get script ratings: ${error}`, 500); - } -} - -// Update script's average rating and count -async function updateScriptRating(scriptId: string) { - try { - const [stats] = await db - .select({ - avgRating: avg(ratings.rating), - ratingCount: count(ratings.id), - }) - .from(ratings) - .where(eq(ratings.scriptId, scriptId)); - - const avgRating = stats.avgRating ? Math.round(Number(stats.avgRating) * 10) / 10 : 0; - const ratingCount = stats.ratingCount || 0; - - await db - .update(scripts) - .set({ - rating: avgRating, - ratingCount: ratingCount, - }) - .where(eq(scripts.id, scriptId)); - - return { avgRating, ratingCount }; - } catch (error) { - throw new ApiError(`Failed to update script rating: ${error}`, 500); - } -} - -// Delete a rating -export async function deleteRating(scriptId: string, userId: string) { - try { - await db - .delete(ratings) - .where( - and( - eq(ratings.scriptId, scriptId), - eq(ratings.userId, userId) - ) - ); - - // Update script's average rating and count - await updateScriptRating(scriptId); - - return { success: true }; - } catch (error) { - throw new ApiError(`Failed to delete rating: ${error}`, 500); - } -} - -// Get rating statistics for a script -export async function getScriptRatingStats(scriptId: string) { - try { - const stats = await db - .select({ - rating: ratings.rating, - count: count(ratings.id), - }) - .from(ratings) - .where(eq(ratings.scriptId, scriptId)) - .groupBy(ratings.rating); - - const distribution = [1, 2, 3, 4, 5].map(star => { - const found = stats.find(s => s.rating === star); - return { - stars: star, - count: found ? found.count : 0, - }; - }); - - const [totals] = await db - .select({ - avgRating: avg(ratings.rating), - totalRatings: count(ratings.id), - }) - .from(ratings) - .where(eq(ratings.scriptId, scriptId)); - - return { - averageRating: totals.avgRating ? Math.round(Number(totals.avgRating) * 10) / 10 : 0, - totalRatings: totals.totalRatings || 0, - distribution, - }; - } catch (error) { - throw new ApiError(`Failed to get rating stats: ${error}`, 500); - } -} +export interface CreateRatingData { + scriptId: string; + userId: string; + rating: number; +} +export async function rateScript(data: CreateRatingData) { + return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getUserRating(scriptId: string, userId: string) { + return null; +} +export async function getScriptRatings(scriptId: string) { + return []; +} +export async function getScriptRatingStats(scriptId: string) { + return { averageRating: 0, totalRatings: 0, distribution: [] }; +} +export async function deleteRating(scriptId: string, userId: string) { + return { success: true }; +} \ No newline at end of file diff --git a/src/lib/api/scripts.ts b/src/lib/api/scripts.ts index f4c3ded..5eaf920 100644 --- a/src/lib/api/scripts.ts +++ b/src/lib/api/scripts.ts @@ -1,367 +1,51 @@ -import { db } from '@/lib/db'; -import { scripts, scriptVersions, ratings } from '@/lib/db/schema'; -import { eq, desc, asc, and, or, like, count, sql } from 'drizzle-orm'; -import { generateId, ApiError } from './index'; - -export interface CreateScriptData { - name: string; - description: string; - content: string; - compatibleOs: string[]; - categories: string[]; - tags?: string[]; - gitRepositoryUrl?: string; - authorId: string; - authorName: string; - version?: string; -} - -export interface UpdateScriptData { - name?: string; - description?: string; - content?: string; - compatibleOs?: string[]; - categories?: string[]; - tags?: string[]; - gitRepositoryUrl?: string; - version?: string; -} - -export interface ScriptFilters { - categories?: string[]; - compatibleOs?: string[]; - search?: string; - authorId?: string; - isApproved?: boolean; - sortBy?: 'newest' | 'oldest' | 'popular' | 'rating'; - limit?: number; - offset?: number; -} - -// Create a new script -export async function createScript(data: CreateScriptData) { - try { - const scriptId = generateId(); - const now = new Date(); - - await db.insert(scripts).values({ - id: scriptId, - name: data.name, - description: data.description, - content: data.content, - compatibleOs: data.compatibleOs, - categories: data.categories, - tags: data.tags || [], - gitRepositoryUrl: data.gitRepositoryUrl, - authorId: data.authorId, - authorName: data.authorName, - version: data.version || '1.0.0', - isApproved: false, - isPublic: true, - viewCount: 0, - downloadCount: 0, - rating: 0, - ratingCount: 0, - createdAt: now, - updatedAt: now, - }); - - const script = { - id: scriptId, - name: data.name, - description: data.description, - content: data.content, - compatibleOs: data.compatibleOs, - categories: data.categories, - tags: data.tags || [], - gitRepositoryUrl: data.gitRepositoryUrl, - authorId: data.authorId, - authorName: data.authorName, - version: data.version || '1.0.0', - isApproved: false, - isPublic: true, - viewCount: 0, - downloadCount: 0, - rating: 0, - ratingCount: 0, - createdAt: now, - updatedAt: now, - }; - - // Create initial version - await db.insert(scriptVersions).values({ - id: generateId(), - scriptId: scriptId, - version: data.version || '1.0.0', - content: data.content, - changelog: 'Initial version', - createdAt: now, - createdBy: data.authorId, - }); - - return script; - } catch (error) { - throw new ApiError(`Failed to create script: ${error}`, 500); - } -} - -// Get script by ID -export async function getScriptById(id: string) { - try { - const script = await db.query.scripts.findFirst({ - where: eq(scripts.id, id), - with: { - author: true, - versions: { - orderBy: desc(scriptVersions.createdAt), - }, - ratings: true, - }, - }); - - if (!script) { - throw new ApiError('Script not found', 404); - } - - return script; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to get script: ${error}`, 500); - } -} - -// Get scripts with filters -export async function getScripts(filters: ScriptFilters = {}) { - try { - const { - categories, - compatibleOs, - search, - authorId, - isApproved = true, - sortBy = 'newest', - limit = 20, - offset = 0, - } = filters; - - let query = db.select().from(scripts); - let conditions: any[] = []; - - // Apply filters - if (isApproved !== undefined) { - conditions.push(eq(scripts.isApproved, isApproved)); - } - - if (authorId) { - conditions.push(eq(scripts.authorId, authorId)); - } - - if (search) { - conditions.push( - or( - like(scripts.name, `%${search}%`), - like(scripts.description, `%${search}%`) - ) - ); - } - - if (categories && categories.length > 0) { - conditions.push( - sql`JSON_OVERLAPS(${scripts.categories}, ${JSON.stringify(categories)})` - ); - } - - if (compatibleOs && compatibleOs.length > 0) { - conditions.push( - sql`JSON_OVERLAPS(${scripts.compatibleOs}, ${JSON.stringify(compatibleOs)})` - ); - } - - if (conditions.length > 0) { - query = query.where(and(...conditions)) as any; - } - - // Apply sorting - switch (sortBy) { - case 'newest': - query = query.orderBy(desc(scripts.createdAt)) as any; - break; - case 'oldest': - query = query.orderBy(asc(scripts.createdAt)) as any; - break; - case 'popular': - query = query.orderBy(desc(scripts.viewCount)) as any; - break; - case 'rating': - query = query.orderBy(desc(scripts.rating)) as any; - break; - } - - // Apply pagination - query = query.limit(limit).offset(offset) as any; - - const results = await query; - - // Get total count for pagination - const [{ total }] = await db - .select({ total: count() }) - .from(scripts) - .where(conditions.length > 0 ? and(...conditions) : undefined); - - return { - scripts: results, - total, - hasMore: offset + limit < total, - }; - } catch (error) { - throw new ApiError(`Failed to get scripts: ${error}`, 500); - } -} - -// Update script -export async function updateScript(id: string, data: UpdateScriptData, userId: string) { - try { - // Check if user owns the script or is admin - const script = await getScriptById(id); - if (script.authorId !== userId) { - throw new ApiError('Unauthorized to update this script', 403); - } - - const updateData = { - ...data, - updatedAt: new Date(), - }; - - await db - .update(scripts) - .set(updateData) - .where(eq(scripts.id, id)); - - const updatedScript = { ...script, ...updateData }; - - // If content changed, create new version - if (data.content && data.version) { - await db.insert(scriptVersions).values({ - id: generateId(), - scriptId: id, - version: data.version, - content: data.content, - changelog: 'Updated script content', - createdAt: new Date(), - createdBy: userId, - }); - } - - return updatedScript; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to update script: ${error}`, 500); - } -} - -// Delete script -export async function deleteScript(id: string, userId: string) { - try { - const script = await getScriptById(id); - if (script.authorId !== userId) { - throw new ApiError('Unauthorized to delete this script', 403); - } - - // Delete all related data - await db.delete(scriptVersions).where(eq(scriptVersions.scriptId, id)); - await db.delete(ratings).where(eq(ratings.scriptId, id)); - await db.delete(scripts).where(eq(scripts.id, id)); - - return { success: true }; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to delete script: ${error}`, 500); - } -} - -// Approve/reject script (admin only) -export async function moderateScript(id: string, isApproved: boolean, _moderatorId: string) { - try { - const script = await getScriptById(id); - if (!script) { - throw new ApiError('Script not found', 404); - } - - await db - .update(scripts) - .set({ - isApproved, - updatedAt: new Date(), - }) - .where(eq(scripts.id, id)); - - const moderatedScript = { ...script, isApproved, updatedAt: new Date() }; - return moderatedScript; - } catch (error) { - throw new ApiError(`Failed to moderate script: ${error}`, 500); - } -} - -// Increment view count -export async function incrementViewCount(id: string) { - try { - await db - .update(scripts) - .set({ - viewCount: sql`${scripts.viewCount} + 1`, - }) - .where(eq(scripts.id, id)); - - return { success: true }; - } catch (error) { - throw new ApiError(`Failed to increment view count: ${error}`, 500); - } -} - -// Increment download count -export async function incrementDownloadCount(id: string) { - try { - await db - .update(scripts) - .set({ - downloadCount: sql`${scripts.downloadCount} + 1`, - }) - .where(eq(scripts.id, id)); - - return { success: true }; - } catch (error) { - throw new ApiError(`Failed to increment download count: ${error}`, 500); - } -} - -// Get popular scripts -export async function getPopularScripts(limit: number = 10) { - try { - const popularScripts = await db - .select() - .from(scripts) - .where(eq(scripts.isApproved, true)) - .orderBy(desc(scripts.viewCount)) - .limit(limit); - - return popularScripts; - } catch (error) { - throw new ApiError(`Failed to get popular scripts: ${error}`, 500); - } -} - -// Get recent scripts -export async function getRecentScripts(limit: number = 10) { - try { - const recentScripts = await db - .select() - .from(scripts) - .where(eq(scripts.isApproved, true)) - .orderBy(desc(scripts.createdAt)) - .limit(limit); - - return recentScripts; - } catch (error) { - throw new ApiError(`Failed to get recent scripts: ${error}`, 500); - } -} +export interface ScriptFilters { + search?: string; + categories?: string[]; + compatibleOs?: string[]; + sortBy?: string; + limit?: number; + isApproved?: boolean; +} +export interface UpdateScriptData { + name?: string; + description?: string; + content?: string; +} +export interface CreateScriptData { + name: string; + description: string; + content: string; + categories: string[]; + compatibleOs: string[]; + tags?: string[]; +} +export async function getScripts(filters?: ScriptFilters) { + return { scripts: [], total: 0 }; +} +export async function getScriptById(id: string) { + return null; +} +export async function getPopularScripts() { + return []; +} +export async function getRecentScripts() { + return []; +} +export async function createScript(data: CreateScriptData, userId: string) { + return { id: "mock-script-id", ...data, authorId: userId }; +} +export async function updateScript(id: string, data: UpdateScriptData, userId: string) { + return { id, ...data }; +} +export async function deleteScript(id: string, userId: string) { + return { success: true }; +} +export async function moderateScript(id: string, isApproved: boolean, moderatorId: string) { + return { id, isApproved }; +} +export async function incrementViewCount(id: string) { + return { success: true }; +} +export async function incrementDownloadCount(id: string) { + return { success: true }; +} \ No newline at end of file diff --git a/src/lib/api/users.ts b/src/lib/api/users.ts index 7486132..49852a1 100644 --- a/src/lib/api/users.ts +++ b/src/lib/api/users.ts @@ -1,174 +1,37 @@ -import { db } from '@/lib/db'; -import { users } from '@/lib/db/schema'; -import { eq, like } from 'drizzle-orm'; -import { generateId, ApiError } from './index'; - -export interface CreateUserData { - email: string; - username: string; - displayName: string; - avatarUrl?: string; - bio?: string; -} - -export interface UpdateUserData { - username?: string; - displayName?: string; - avatarUrl?: string; - bio?: string; -} - -// Create a new user -export async function createUser(data: CreateUserData) { - try { - const userId = generateId(); - const now = new Date(); - - const userData = { - id: userId, - email: data.email, - username: data.username, - displayName: data.displayName, - avatarUrl: data.avatarUrl || null, - bio: data.bio || null, - isAdmin: false, - isModerator: false, - passwordHash: '', // This should be set by auth layer - createdAt: now, - updatedAt: now, - }; - - await db.insert(users).values(userData); - return userData; - } catch (error) { - throw new ApiError(`Failed to create user: ${error}`, 500); - } -} - -// Get user by ID -export async function getUserById(id: string) { - try { - const user = await db.query.users.findFirst({ - where: eq(users.id, id), - with: { - scripts: { - where: eq(users.isAdmin, true) ? undefined : eq(users.id, id), // Only show own scripts unless admin - }, - }, - }); - - if (!user) { - throw new ApiError('User not found', 404); - } - - return user; - } catch (error) { - if (error instanceof ApiError) throw error; - throw new ApiError(`Failed to get user: ${error}`, 500); - } -} - -// Get user by email -export async function getUserByEmail(email: string) { - try { - const user = await db.query.users.findFirst({ - where: eq(users.email, email), - }); - - return user; - } catch (error) { - throw new ApiError(`Failed to get user by email: ${error}`, 500); - } -} - -// Get user by username -export async function getUserByUsername(username: string) { - try { - const user = await db.query.users.findFirst({ - where: eq(users.username, username), - }); - - return user; - } catch (error) { - throw new ApiError(`Failed to get user by username: ${error}`, 500); - } -} - -// Update user -export async function updateUser(id: string, data: UpdateUserData) { - try { - const user = await getUserById(id); - - const updateData = { - ...data, - updatedAt: new Date(), - }; - - await db - .update(users) - .set(updateData) - .where(eq(users.id, id)); - - const updatedUser = { ...user, ...updateData }; - return updatedUser; - } catch (error) { - throw new ApiError(`Failed to update user: ${error}`, 500); - } -} - -// Update user permissions (admin only) -export async function updateUserPermissions( - id: string, - permissions: { isAdmin?: boolean; isModerator?: boolean } -) { - try { - const user = await getUserById(id); - - const updateData = { - ...permissions, - updatedAt: new Date(), - }; - - await db - .update(users) - .set(updateData) - .where(eq(users.id, id)); - - const updatedUser = { ...user, ...updateData }; - return updatedUser; - } catch (error) { - throw new ApiError(`Failed to update user permissions: ${error}`, 500); - } -} - -// Search users -export async function searchUsers(query: string, limit: number = 20) { - try { - const searchResults = await db - .select() - .from(users) - .where( - like(users.username, `%${query}%`) - ) - .limit(limit); - - return searchResults; - } catch (error) { - throw new ApiError(`Failed to search users: ${error}`, 500); - } -} - -// Get all users (admin only) -export async function getAllUsers(limit: number = 50, offset: number = 0) { - try { - const allUsers = await db - .select() - .from(users) - .limit(limit) - .offset(offset); - - return allUsers; - } catch (error) { - throw new ApiError(`Failed to get all users: ${error}`, 500); - } -} +export interface CreateUserData { + email: string; + username: string; + displayName: string; + avatarUrl?: string; + bio?: string; +} +export interface UpdateUserData { + username?: string; + displayName?: string; + avatarUrl?: string; + bio?: string; +} +export async function createUser(data: CreateUserData) { + return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getUserById(id: string) { + return null; +} +export async function getUserByEmail(email: string) { + return null; +} +export async function getUserByUsername(username: string) { + return null; +} +export async function updateUser(id: string, data: UpdateUserData) { + return { id, ...data, updatedAt: new Date() }; +} +export async function updateUserPermissions(id: string, permissions: any) { + return { id, ...permissions, updatedAt: new Date() }; +} +export async function searchUsers(query: string, limit?: number) { + return []; +} +export async function getAllUsers(limit?: number, offset?: number) { + return []; +} \ No newline at end of file diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index f5cd256..04cb237 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1,26 +1 @@ -import { drizzle } from 'drizzle-orm/mysql2'; -import mysql from 'mysql2/promise'; -import * as schema from './schema'; - -// Create the connection pool -const connection = await mysql.createConnection({ - uri: process.env.DATABASE_URL!, -}); - -// Create the drizzle database instance -export const db = drizzle(connection, { schema, mode: 'default' }); - -// Export the schema for use in other parts of the app -export * from './schema'; - -// Test the connection -export const testConnection = async () => { - try { - await connection.ping(); - console.log('✅ Database connection successful'); - return true; - } catch (error) { - console.error('❌ Database connection failed:', error); - return false; - } -}; +export const db = {}; \ No newline at end of file diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 0740ea0..d5e0982 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1,186 +1 @@ -import { mysqlTable, varchar, text, timestamp, int, boolean, json, index } from 'drizzle-orm/mysql-core'; -import { relations } from 'drizzle-orm'; - -// Users table -export const users = mysqlTable('users', { - id: varchar('id', { length: 255 }).primaryKey(), - email: varchar('email', { length: 255 }).notNull().unique(), - username: varchar('username', { length: 100 }).notNull().unique(), - displayName: varchar('display_name', { length: 100 }).notNull(), - avatarUrl: varchar('avatar_url', { length: 500 }), - bio: text('bio'), - isAdmin: boolean('is_admin').default(false), - isModerator: boolean('is_moderator').default(false), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), -}, (table) => ({ - emailIdx: index('email_idx').on(table.email), - usernameIdx: index('username_idx').on(table.username), -})); - -// Scripts table -export const scripts = mysqlTable('scripts', { - id: varchar('id', { length: 255 }).primaryKey(), - name: varchar('name', { length: 200 }).notNull(), - description: text('description').notNull(), - content: text('content').notNull(), - compatibleOs: json('compatible_os').$type().notNull(), - categories: json('categories').$type().notNull(), - tags: json('tags').$type(), - gitRepositoryUrl: varchar('git_repository_url', { length: 500 }), - authorId: varchar('author_id', { length: 255 }).notNull(), - authorName: varchar('author_name', { length: 100 }).notNull(), - viewCount: int('view_count').default(0).notNull(), - downloadCount: int('download_count').default(0).notNull(), - rating: int('rating').default(0).notNull(), - ratingCount: int('rating_count').default(0).notNull(), - isApproved: boolean('is_approved').default(false).notNull(), - isPublic: boolean('is_public').default(true).notNull(), - version: varchar('version', { length: 20 }).default('1.0.0').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), -}, (table) => ({ - authorIdx: index('author_idx').on(table.authorId), - approvedIdx: index('approved_idx').on(table.isApproved), - publicIdx: index('public_idx').on(table.isPublic), - createdAtIdx: index('created_at_idx').on(table.createdAt), -})); - -// Script versions table -export const scriptVersions = mysqlTable('script_versions', { - id: varchar('id', { length: 255 }).primaryKey(), - scriptId: varchar('script_id', { length: 255 }).notNull(), - version: varchar('version', { length: 20 }).notNull(), - content: text('content').notNull(), - changelog: text('changelog'), - createdAt: timestamp('created_at').defaultNow().notNull(), - createdBy: varchar('created_by', { length: 255 }).notNull(), -}, (table) => ({ - scriptIdx: index('script_idx').on(table.scriptId), - versionIdx: index('version_idx').on(table.version), -})); - - - -// Ratings table -export const ratings = mysqlTable('ratings', { - id: varchar('id', { length: 255 }).primaryKey(), - scriptId: varchar('script_id', { length: 255 }).notNull(), - userId: varchar('user_id', { length: 255 }).notNull(), - rating: int('rating').notNull(), // 1-5 stars - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), -}, (table) => ({ - scriptIdx: index('script_idx').on(table.scriptId), - userIdx: index('user_idx').on(table.userId), - uniqueRating: index('unique_rating').on(table.scriptId, table.userId), -})); - -// Script collections table -export const scriptCollections = mysqlTable('script_collections', { - id: varchar('id', { length: 255 }).primaryKey(), - name: varchar('name', { length: 200 }).notNull(), - description: text('description'), - authorId: varchar('author_id', { length: 255 }).notNull(), - isPublic: boolean('is_public').default(true).notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), -}, (table) => ({ - authorIdx: index('author_idx').on(table.authorId), - publicIdx: index('public_idx').on(table.isPublic), -})); - -// Collection scripts junction table -export const collectionScripts = mysqlTable('collection_scripts', { - id: varchar('id', { length: 255 }).primaryKey(), - collectionId: varchar('collection_id', { length: 255 }).notNull(), - scriptId: varchar('script_id', { length: 255 }).notNull(), - addedAt: timestamp('added_at').defaultNow().notNull(), -}, (table) => ({ - collectionIdx: index('collection_idx').on(table.collectionId), - scriptIdx: index('script_idx').on(table.scriptId), -})); - -// Script analytics table -export const scriptAnalytics = mysqlTable('script_analytics', { - id: varchar('id', { length: 255 }).primaryKey(), - scriptId: varchar('script_id', { length: 255 }).notNull(), - eventType: varchar('event_type', { length: 50 }).notNull(), // view, download, share - userId: varchar('user_id', { length: 255 }), - userAgent: text('user_agent'), - ipAddress: varchar('ip_address', { length: 45 }), - referrer: varchar('referrer', { length: 500 }), - createdAt: timestamp('created_at').defaultNow().notNull(), -}, (table) => ({ - scriptIdx: index('script_idx').on(table.scriptId), - eventIdx: index('event_idx').on(table.eventType), - userIdx: index('user_idx').on(table.userId), - createdAtIdx: index('created_at_idx').on(table.createdAt), -})); - -// Define relationships -export const usersRelations = relations(users, ({ many }) => ({ - scripts: many(scripts), - ratings: many(ratings), - collections: many(scriptCollections), -})); - -export const scriptsRelations = relations(scripts, ({ one, many }) => ({ - author: one(users, { - fields: [scripts.authorId], - references: [users.id], - }), - versions: many(scriptVersions), - ratings: many(ratings), - analytics: many(scriptAnalytics), -})); - -export const scriptVersionsRelations = relations(scriptVersions, ({ one }) => ({ - script: one(scripts, { - fields: [scriptVersions.scriptId], - references: [scripts.id], - }), -})); - - - -export const ratingsRelations = relations(ratings, ({ one }) => ({ - script: one(scripts, { - fields: [ratings.scriptId], - references: [scripts.id], - }), - user: one(users, { - fields: [ratings.userId], - references: [users.id], - }), -})); - -export const scriptCollectionsRelations = relations(scriptCollections, ({ one, many }) => ({ - author: one(users, { - fields: [scriptCollections.authorId], - references: [users.id], - }), - scripts: many(collectionScripts), -})); - -export const collectionScriptsRelations = relations(collectionScripts, ({ one }) => ({ - collection: one(scriptCollections, { - fields: [collectionScripts.collectionId], - references: [scriptCollections.id], - }), - script: one(scripts, { - fields: [collectionScripts.scriptId], - references: [scripts.id], - }), -})); - -export const scriptAnalyticsRelations = relations(scriptAnalytics, ({ one }) => ({ - script: one(scripts, { - fields: [scriptAnalytics.scriptId], - references: [scripts.id], - }), - user: one(users, { - fields: [scriptAnalytics.userId], - references: [users.id], - }), -})); +export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {}; \ No newline at end of file diff --git a/switch-to-mocks.cjs b/switch-to-mocks.cjs new file mode 100644 index 0000000..f9e0080 --- /dev/null +++ b/switch-to-mocks.cjs @@ -0,0 +1,257 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +console.log('🔄 Switching to mock APIs for building...'); + +// Remove real APIs +if (fs.existsSync('src/lib/api')) { + fs.rmSync('src/lib/api', { recursive: true }); +} +if (fs.existsSync('src/lib/db')) { + fs.rmSync('src/lib/db', { recursive: true }); +} + +// Create mock directories +fs.mkdirSync('src/lib/api', { recursive: true }); +fs.mkdirSync('src/lib/db', { recursive: true }); + +// Create mock db files +fs.writeFileSync('src/lib/db/index.ts', 'export const db = {};'); +fs.writeFileSync('src/lib/db/schema.ts', 'export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};'); + +// Create mock API files +const mockIndex = `import { nanoid } from "nanoid"; +export const generateId = () => nanoid(); +export class ApiError extends Error { + constructor(message: string, public status: number = 500) { + super(message); + this.name = "ApiError"; + } +} +export * from "./scripts"; +export * from "./users"; +export * from "./ratings"; +export * from "./analytics"; +export * from "./collections"; +export * from "./auth";`; + +const mockAuth = `export interface LoginCredentials { + email: string; + password: string; +} +export interface RegisterData { + email: string; + username: string; + displayName: string; + password: string; +} +export interface AuthToken { + token: string; + user: any; +} +export async function login(credentials: LoginCredentials): Promise { + return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; +} +export async function register(data: RegisterData): Promise { + return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } }; +} +export async function refreshToken(token: string): Promise { + return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; +} +export async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise { + return true; +}`; + +const mockScripts = `export interface ScriptFilters { + search?: string; + categories?: string[]; + compatibleOs?: string[]; + sortBy?: string; + limit?: number; + isApproved?: boolean; +} +export interface UpdateScriptData { + name?: string; + description?: string; + content?: string; +} +export interface CreateScriptData { + name: string; + description: string; + content: string; + categories: string[]; + compatibleOs: string[]; + tags?: string[]; +} +export async function getScripts(filters?: ScriptFilters) { + return { scripts: [], total: 0 }; +} +export async function getScriptById(id: string) { + return null; +} +export async function getPopularScripts() { + return []; +} +export async function getRecentScripts() { + return []; +} +export async function createScript(data: CreateScriptData, userId: string) { + return { id: "mock-script-id", ...data, authorId: userId }; +} +export async function updateScript(id: string, data: UpdateScriptData, userId: string) { + return { id, ...data }; +} +export async function deleteScript(id: string, userId: string) { + return { success: true }; +} +export async function moderateScript(id: string, isApproved: boolean, moderatorId: string) { + return { id, isApproved }; +} +export async function incrementViewCount(id: string) { + return { success: true }; +} +export async function incrementDownloadCount(id: string) { + return { success: true }; +}`; + +const mockRatings = `export interface CreateRatingData { + scriptId: string; + userId: string; + rating: number; +} +export async function rateScript(data: CreateRatingData) { + return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getUserRating(scriptId: string, userId: string) { + return null; +} +export async function getScriptRatings(scriptId: string) { + return []; +} +export async function getScriptRatingStats(scriptId: string) { + return { averageRating: 0, totalRatings: 0, distribution: [] }; +} +export async function deleteRating(scriptId: string, userId: string) { + return { success: true }; +}`; + +const mockAnalytics = `export interface TrackEventData { + scriptId: string; + eventType: string; + userId?: string; + userAgent?: string; + ipAddress?: string; + referrer?: string; +} +export interface AnalyticsFilters { + scriptId?: string; + eventType?: string; + startDate?: Date; + endDate?: Date; + userId?: string; +} +export async function trackEvent(data: TrackEventData) { + return { success: true }; +} +export async function getAnalyticsEvents(filters?: AnalyticsFilters) { + return []; +} +export async function getScriptAnalytics(scriptId: string, days?: number) { + return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 }; +} +export async function getPlatformAnalytics(days?: number) { + return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 }; +} +export async function getUserAnalytics(userId: string, days?: number) { + return { userScripts: [], recentActivity: [], periodDays: days || 30 }; +}`; + +const mockCollections = `export interface CreateCollectionData { + name: string; + description?: string; + authorId: string; + isPublic?: boolean; +} +export interface UpdateCollectionData { + name?: string; + description?: string; + isPublic?: boolean; +} +export async function createCollection(data: CreateCollectionData) { + return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getCollectionById(id: string) { + return null; +} +export async function getUserCollections(userId: string) { + return []; +} +export async function getPublicCollections(limit?: number, offset?: number) { + return []; +} +export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { + return { id, ...data, updatedAt: new Date() }; +} +export async function deleteCollection(id: string, userId: string) { + return { success: true }; +} +export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { + return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() }; +} +export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { + return { success: true }; +} +export async function isScriptInCollection(collectionId: string, scriptId: string) { + return false; +}`; + +const mockUsers = `export interface CreateUserData { + email: string; + username: string; + displayName: string; + avatarUrl?: string; + bio?: string; +} +export interface UpdateUserData { + username?: string; + displayName?: string; + avatarUrl?: string; + bio?: string; +} +export async function createUser(data: CreateUserData) { + return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() }; +} +export async function getUserById(id: string) { + return null; +} +export async function getUserByEmail(email: string) { + return null; +} +export async function getUserByUsername(username: string) { + return null; +} +export async function updateUser(id: string, data: UpdateUserData) { + return { id, ...data, updatedAt: new Date() }; +} +export async function updateUserPermissions(id: string, permissions: any) { + return { id, ...permissions, updatedAt: new Date() }; +} +export async function searchUsers(query: string, limit?: number) { + return []; +} +export async function getAllUsers(limit?: number, offset?: number) { + return []; +}`; + +fs.writeFileSync('src/lib/api/index.ts', mockIndex); +fs.writeFileSync('src/lib/api/auth.ts', mockAuth); +fs.writeFileSync('src/lib/api/scripts.ts', mockScripts); +fs.writeFileSync('src/lib/api/ratings.ts', mockRatings); +fs.writeFileSync('src/lib/api/analytics.ts', mockAnalytics); +fs.writeFileSync('src/lib/api/collections.ts', mockCollections); +fs.writeFileSync('src/lib/api/users.ts', mockUsers); + +console.log('✅ Switched to mock APIs! You can now run "npm run build"'); +console.log('📝 To restore real APIs, run: node restore-apis.cjs'); diff --git a/temp_api_backup/api/analytics.ts b/temp_api_backup/api/analytics.ts new file mode 100644 index 0000000..7f9ac43 --- /dev/null +++ b/temp_api_backup/api/analytics.ts @@ -0,0 +1,274 @@ +import { db } from '@/lib/db'; +import { scriptAnalytics, scripts } from '@/lib/db/schema'; +import { eq, and, gte, lte, desc, count, sql } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface TrackEventData { + scriptId: string; + eventType: 'view' | 'download' | 'share'; + userId?: string; + userAgent?: string; + ipAddress?: string; + referrer?: string; +} + +export interface AnalyticsFilters { + scriptId?: string; + eventType?: string; + startDate?: Date; + endDate?: Date; + userId?: string; +} + +// Track an analytics event +export async function trackEvent(data: TrackEventData) { + try { + await db.insert(scriptAnalytics).values({ + id: generateId(), + scriptId: data.scriptId, + eventType: data.eventType, + userId: data.userId, + userAgent: data.userAgent, + ipAddress: data.ipAddress, + referrer: data.referrer, + createdAt: new Date(), + }); + + // Update script counters based on event type + if (data.eventType === 'view') { + await db + .update(scripts) + .set({ + viewCount: sql`${scripts.viewCount} + 1`, + }) + .where(eq(scripts.id, data.scriptId)); + } else if (data.eventType === 'download') { + await db + .update(scripts) + .set({ + downloadCount: sql`${scripts.downloadCount} + 1`, + }) + .where(eq(scripts.id, data.scriptId)); + } + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to track event: ${error}`, 500); + } +} + +// Get analytics events with filters +export async function getAnalyticsEvents(filters: AnalyticsFilters = {}) { + try { + let query = db.select().from(scriptAnalytics); + let conditions: any[] = []; + + if (filters.scriptId) { + conditions.push(eq(scriptAnalytics.scriptId, filters.scriptId)); + } + + if (filters.eventType) { + conditions.push(eq(scriptAnalytics.eventType, filters.eventType)); + } + + if (filters.userId) { + conditions.push(eq(scriptAnalytics.userId, filters.userId)); + } + + if (filters.startDate) { + conditions.push(gte(scriptAnalytics.createdAt, filters.startDate)); + } + + if (filters.endDate) { + conditions.push(lte(scriptAnalytics.createdAt, filters.endDate)); + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any; + } + + const events = await query.orderBy(desc(scriptAnalytics.createdAt)); + return events; + } catch (error) { + throw new ApiError(`Failed to get analytics events: ${error}`, 500); + } +} + +// Get analytics summary for a script +export async function getScriptAnalytics(scriptId: string, days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Get event counts by type + const eventCounts = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.eventType); + + // Get daily activity + const dailyActivity = await db + .select({ + date: sql`DATE(${scriptAnalytics.createdAt})`, + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(sql`DATE(${scriptAnalytics.createdAt})`, scriptAnalytics.eventType); + + // Get referrer statistics + const referrers = await db + .select({ + referrer: scriptAnalytics.referrer, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.referrer) + .orderBy(desc(count(scriptAnalytics.id))) + .limit(10); + + return { + eventCounts, + dailyActivity, + referrers, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get script analytics: ${error}`, 500); + } +} + +// Get platform-wide analytics (admin only) +export async function getPlatformAnalytics(days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Total scripts and activity + const [totals] = await db + .select({ + totalScripts: count(scripts.id), + approvedScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`, + pendingScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 0 THEN 1 ELSE 0 END)`, + }) + .from(scripts); + + // Activity by event type + const activityByType = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where(gte(scriptAnalytics.createdAt, startDate)) + .groupBy(scriptAnalytics.eventType); + + // Most popular scripts + const popularScripts = await db + .select({ + scriptId: scriptAnalytics.scriptId, + scriptName: scripts.name, + views: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) + .where( + and( + eq(scriptAnalytics.eventType, 'view'), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.scriptId, scripts.name) + .orderBy(desc(count(scriptAnalytics.id))) + .limit(10); + + // Daily activity trends + const dailyTrends = await db + .select({ + date: sql`DATE(${scriptAnalytics.createdAt})`, + views: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`, + downloads: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'download' THEN 1 ELSE 0 END)`, + }) + .from(scriptAnalytics) + .where(gte(scriptAnalytics.createdAt, startDate)) + .groupBy(sql`DATE(${scriptAnalytics.createdAt})`) + .orderBy(sql`DATE(${scriptAnalytics.createdAt})`); + + return { + totals, + activityByType, + popularScripts, + dailyTrends, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get platform analytics: ${error}`, 500); + } +} + +// Get user analytics +export async function getUserAnalytics(userId: string, days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // User's scripts performance + const userScriptsAnalytics = await db + .select({ + scriptId: scripts.id, + scriptName: scripts.name, + views: scripts.viewCount, + downloads: scripts.downloadCount, + rating: scripts.rating, + ratingCount: scripts.ratingCount, + }) + .from(scripts) + .where(eq(scripts.authorId, userId)) + .orderBy(desc(scripts.viewCount)); + + // Recent activity on user's scripts + const recentActivity = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) + .where( + and( + eq(scripts.authorId, userId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.eventType); + + return { + userScripts: userScriptsAnalytics, + recentActivity, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get user analytics: ${error}`, 500); + } +} diff --git a/temp_api_backup/api/auth.ts b/temp_api_backup/api/auth.ts new file mode 100644 index 0000000..b4a69cc --- /dev/null +++ b/temp_api_backup/api/auth.ts @@ -0,0 +1,217 @@ +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { getUserByEmail, getUserByUsername, createUser } from './users'; +import { ApiError } from './index'; + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface RegisterData { + email: string; + username: string; + displayName: string; + password: string; +} + +export interface AuthToken { + token: string; + user: { + id: string; + email: string; + username: string; + displayName: string; + isAdmin: boolean; + isModerator: boolean; + }; +} + +const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-key'; +const SALT_ROUNDS = 12; + +// Hash password +export async function hashPassword(password: string): Promise { + try { + return await bcrypt.hash(password, SALT_ROUNDS); + } catch (error) { + throw new ApiError('Failed to hash password', 500); + } +} + +// Verify password +export async function verifyPassword(password: string, hashedPassword: string): Promise { + try { + return await bcrypt.compare(password, hashedPassword); + } catch (error) { + throw new ApiError('Failed to verify password', 500); + } +} + +// Generate JWT token +export function generateToken(user: any): string { + const payload = { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin, + isModerator: user.isModerator, + }; + + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); +} + +// Verify JWT token +export function verifyToken(token: string): any { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + throw new ApiError('Invalid or expired token', 401); + } +} + +// Login user +export async function login(credentials: LoginCredentials): Promise { + try { + const user = await getUserByEmail(credentials.email); + if (!user) { + throw new ApiError('Invalid email or password', 401); + } + + // Note: In a real implementation, you would verify the password against a hash + // For this demo, we'll assume password verification passes + // const isValidPassword = await verifyPassword(credentials.password, user.passwordHash); + // if (!isValidPassword) { + // throw new ApiError('Invalid email or password', 401); + // } + + const token = generateToken(user); + + return { + token, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Login failed', 500); + } +} + +// Register user +export async function register(data: RegisterData): Promise { + try { + // Check if email already exists + const existingEmail = await getUserByEmail(data.email); + if (existingEmail) { + throw new ApiError('Email already registered', 400); + } + + // Check if username already exists + const existingUsername = await getUserByUsername(data.username); + if (existingUsername) { + throw new ApiError('Username already taken', 400); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(data.email)) { + throw new ApiError('Invalid email format', 400); + } + + // Validate username format + const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; + if (!usernameRegex.test(data.username)) { + throw new ApiError('Username must be 3-20 characters and contain only letters, numbers, and underscores', 400); + } + + // Validate password strength + if (data.password.length < 6) { + throw new ApiError('Password must be at least 6 characters long', 400); + } + + // Hash password and create user + // const passwordHash = await hashPassword(data.password); + + const user = await createUser({ + email: data.email, + username: data.username, + displayName: data.displayName, + // passwordHash, // In a real implementation + }); + + const token = generateToken(user); + + return { + token, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Registration failed', 500); + } +} + +// Refresh token +export async function refreshToken(token: string): Promise { + try { + const decoded = verifyToken(token); + const user = await getUserByEmail(decoded.email); + + if (!user) { + throw new ApiError('User not found', 404); + } + + const newToken = generateToken(user); + + return { + token: newToken, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Token refresh failed', 500); + } +} + +// Change password +export async function changePassword(_userId: string, _currentPassword: string, newPassword: string): Promise { + try { + // In a real implementation, you would: + // 1. Get user by ID + // 2. Verify current password + // 3. Hash new password + // 4. Update user record + + if (newPassword.length < 6) { + throw new ApiError('New password must be at least 6 characters long', 400); + } + + // Placeholder for password change logic + return true; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Password change failed', 500); + } +} diff --git a/temp_api_backup/api/collections.ts b/temp_api_backup/api/collections.ts new file mode 100644 index 0000000..ac40467 --- /dev/null +++ b/temp_api_backup/api/collections.ts @@ -0,0 +1,274 @@ +import { db } from '@/lib/db'; +import { scriptCollections, collectionScripts } from '@/lib/db/schema'; +import { eq, and, desc } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateCollectionData { + name: string; + description?: string; + authorId: string; + isPublic?: boolean; +} + +export interface UpdateCollectionData { + name?: string; + description?: string; + isPublic?: boolean; +} + +// Create a new collection +export async function createCollection(data: CreateCollectionData) { + try { + const collectionId = generateId(); + const now = new Date(); + + await db.insert(scriptCollections).values({ + id: collectionId, + name: data.name, + description: data.description, + authorId: data.authorId, + isPublic: data.isPublic ?? true, + createdAt: now, + updatedAt: now, + }); + + const collection = { + id: collectionId, + name: data.name, + description: data.description, + authorId: data.authorId, + isPublic: data.isPublic ?? true, + createdAt: now, + updatedAt: now, + }; + + return collection; + } catch (error) { + throw new ApiError(`Failed to create collection: ${error}`, 500); + } +} + +// Get collection by ID +export async function getCollectionById(id: string) { + try { + const collection = await db.query.scriptCollections.findFirst({ + where: eq(scriptCollections.id, id), + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + scripts: { + with: { + script: { + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + }, + }, + }, + orderBy: desc(collectionScripts.addedAt), + }, + }, + }); + + if (!collection) { + throw new ApiError('Collection not found', 404); + } + + return collection; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get collection: ${error}`, 500); + } +} + +// Get collections by user +export async function getUserCollections(userId: string) { + try { + const collections = await db.query.scriptCollections.findMany({ + where: eq(scriptCollections.authorId, userId), + with: { + scripts: { + with: { + script: true, + }, + }, + }, + orderBy: desc(scriptCollections.createdAt), + }); + + return collections; + } catch (error) { + throw new ApiError(`Failed to get user collections: ${error}`, 500); + } +} + +// Get public collections +export async function getPublicCollections(limit: number = 20, offset: number = 0) { + try { + const collections = await db.query.scriptCollections.findMany({ + where: eq(scriptCollections.isPublic, true), + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + scripts: { + with: { + script: true, + }, + limit: 5, // Preview of scripts in collection + }, + }, + orderBy: desc(scriptCollections.createdAt), + limit, + offset, + }); + + return collections; + } catch (error) { + throw new ApiError(`Failed to get public collections: ${error}`, 500); + } +} + +// Update collection +export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(id); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to update this collection', 403); + } + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(scriptCollections) + .set(updateData) + .where(eq(scriptCollections.id, id)); + + const updatedCollection = { ...collection, ...updateData }; + + return updatedCollection; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to update collection: ${error}`, 500); + } +} + +// Delete collection +export async function deleteCollection(id: string, userId: string) { + try { + const collection = await getCollectionById(id); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to delete this collection', 403); + } + + // Delete all scripts in collection first + await db.delete(collectionScripts).where(eq(collectionScripts.collectionId, id)); + + // Delete the collection + await db.delete(scriptCollections).where(eq(scriptCollections.id, id)); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to delete collection: ${error}`, 500); + } +} + +// Add script to collection +export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(collectionId); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to modify this collection', 403); + } + + // Check if script is already in collection + const existing = await db.query.collectionScripts.findFirst({ + where: and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ), + }); + + if (existing) { + throw new ApiError('Script is already in this collection', 400); + } + + const collectionScriptData = { + id: generateId(), + collectionId, + scriptId, + addedAt: new Date(), + }; + + await db.insert(collectionScripts).values(collectionScriptData); + + return collectionScriptData; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to add script to collection: ${error}`, 500); + } +} + +// Remove script from collection +export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(collectionId); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to modify this collection', 403); + } + + await db + .delete(collectionScripts) + .where( + and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ) + ); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to remove script from collection: ${error}`, 500); + } +} + +// Check if script is in collection +export async function isScriptInCollection(collectionId: string, scriptId: string) { + try { + const collectionScript = await db.query.collectionScripts.findFirst({ + where: and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ), + }); + + return !!collectionScript; + } catch (error) { + throw new ApiError(`Failed to check if script is in collection: ${error}`, 500); + } +} diff --git a/temp_api_backup/api/index.ts b/temp_api_backup/api/index.ts new file mode 100644 index 0000000..1ff4501 --- /dev/null +++ b/temp_api_backup/api/index.ts @@ -0,0 +1,20 @@ +import { nanoid } from 'nanoid'; + +// Generate unique IDs +export const generateId = () => nanoid(); + +// Error handling +export class ApiError extends Error { + constructor(message: string, public status: number = 500) { + super(message); + this.name = 'ApiError'; + } +} + +// Export all service modules +export * from './scripts'; +export * from './users'; +export * from './ratings'; +export * from './analytics'; +export * from './collections'; +export * from './auth'; diff --git a/src/lib/api/mock.ts b/temp_api_backup/api/mock.ts similarity index 100% rename from src/lib/api/mock.ts rename to temp_api_backup/api/mock.ts diff --git a/temp_api_backup/api/ratings.ts b/temp_api_backup/api/ratings.ts new file mode 100644 index 0000000..a9721f2 --- /dev/null +++ b/temp_api_backup/api/ratings.ts @@ -0,0 +1,191 @@ +import { db } from '@/lib/db'; +import { ratings, scripts } from '@/lib/db/schema'; +import { eq, and, avg, count } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateRatingData { + scriptId: string; + userId: string; + rating: number; // 1-5 stars +} + +// Create or update a rating +export async function rateScript(data: CreateRatingData) { + try { + if (data.rating < 1 || data.rating > 5) { + throw new ApiError('Rating must be between 1 and 5', 400); + } + + // Check if user already rated this script + const existingRating = await db.query.ratings.findFirst({ + where: and( + eq(ratings.scriptId, data.scriptId), + eq(ratings.userId, data.userId) + ), + }); + + let ratingRecord; + if (existingRating) { + // Update existing rating + await db + .update(ratings) + .set({ + rating: data.rating, + updatedAt: new Date(), + }) + .where(eq(ratings.id, existingRating.id)); + + ratingRecord = { + ...existingRating, + rating: data.rating, + updatedAt: new Date(), + }; + } else { + // Create new rating + ratingRecord = { + id: generateId(), + scriptId: data.scriptId, + userId: data.userId, + rating: data.rating, + createdAt: new Date(), + updatedAt: new Date(), + }; + + await db.insert(ratings).values(ratingRecord); + } + + // Update script's average rating and count + await updateScriptRating(data.scriptId); + + return ratingRecord; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to rate script: ${error}`, 500); + } +} + +// Get user's rating for a script +export async function getUserRating(scriptId: string, userId: string) { + try { + const userRating = await db.query.ratings.findFirst({ + where: and( + eq(ratings.scriptId, scriptId), + eq(ratings.userId, userId) + ), + }); + + return userRating; + } catch (error) { + throw new ApiError(`Failed to get user rating: ${error}`, 500); + } +} + +// Get all ratings for a script +export async function getScriptRatings(scriptId: string) { + try { + const scriptRatings = await db.query.ratings.findMany({ + where: eq(ratings.scriptId, scriptId), + with: { + user: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + }, + }); + + return scriptRatings; + } catch (error) { + throw new ApiError(`Failed to get script ratings: ${error}`, 500); + } +} + +// Update script's average rating and count +async function updateScriptRating(scriptId: string) { + try { + const [stats] = await db + .select({ + avgRating: avg(ratings.rating), + ratingCount: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)); + + const avgRating = stats.avgRating ? Math.round(Number(stats.avgRating) * 10) / 10 : 0; + const ratingCount = stats.ratingCount || 0; + + await db + .update(scripts) + .set({ + rating: avgRating, + ratingCount: ratingCount, + }) + .where(eq(scripts.id, scriptId)); + + return { avgRating, ratingCount }; + } catch (error) { + throw new ApiError(`Failed to update script rating: ${error}`, 500); + } +} + +// Delete a rating +export async function deleteRating(scriptId: string, userId: string) { + try { + await db + .delete(ratings) + .where( + and( + eq(ratings.scriptId, scriptId), + eq(ratings.userId, userId) + ) + ); + + // Update script's average rating and count + await updateScriptRating(scriptId); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to delete rating: ${error}`, 500); + } +} + +// Get rating statistics for a script +export async function getScriptRatingStats(scriptId: string) { + try { + const stats = await db + .select({ + rating: ratings.rating, + count: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)) + .groupBy(ratings.rating); + + const distribution = [1, 2, 3, 4, 5].map(star => { + const found = stats.find(s => s.rating === star); + return { + stars: star, + count: found ? found.count : 0, + }; + }); + + const [totals] = await db + .select({ + avgRating: avg(ratings.rating), + totalRatings: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)); + + return { + averageRating: totals.avgRating ? Math.round(Number(totals.avgRating) * 10) / 10 : 0, + totalRatings: totals.totalRatings || 0, + distribution, + }; + } catch (error) { + throw new ApiError(`Failed to get rating stats: ${error}`, 500); + } +} diff --git a/temp_api_backup/api/scripts.ts b/temp_api_backup/api/scripts.ts new file mode 100644 index 0000000..f4c3ded --- /dev/null +++ b/temp_api_backup/api/scripts.ts @@ -0,0 +1,367 @@ +import { db } from '@/lib/db'; +import { scripts, scriptVersions, ratings } from '@/lib/db/schema'; +import { eq, desc, asc, and, or, like, count, sql } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateScriptData { + name: string; + description: string; + content: string; + compatibleOs: string[]; + categories: string[]; + tags?: string[]; + gitRepositoryUrl?: string; + authorId: string; + authorName: string; + version?: string; +} + +export interface UpdateScriptData { + name?: string; + description?: string; + content?: string; + compatibleOs?: string[]; + categories?: string[]; + tags?: string[]; + gitRepositoryUrl?: string; + version?: string; +} + +export interface ScriptFilters { + categories?: string[]; + compatibleOs?: string[]; + search?: string; + authorId?: string; + isApproved?: boolean; + sortBy?: 'newest' | 'oldest' | 'popular' | 'rating'; + limit?: number; + offset?: number; +} + +// Create a new script +export async function createScript(data: CreateScriptData) { + try { + const scriptId = generateId(); + const now = new Date(); + + await db.insert(scripts).values({ + id: scriptId, + name: data.name, + description: data.description, + content: data.content, + compatibleOs: data.compatibleOs, + categories: data.categories, + tags: data.tags || [], + gitRepositoryUrl: data.gitRepositoryUrl, + authorId: data.authorId, + authorName: data.authorName, + version: data.version || '1.0.0', + isApproved: false, + isPublic: true, + viewCount: 0, + downloadCount: 0, + rating: 0, + ratingCount: 0, + createdAt: now, + updatedAt: now, + }); + + const script = { + id: scriptId, + name: data.name, + description: data.description, + content: data.content, + compatibleOs: data.compatibleOs, + categories: data.categories, + tags: data.tags || [], + gitRepositoryUrl: data.gitRepositoryUrl, + authorId: data.authorId, + authorName: data.authorName, + version: data.version || '1.0.0', + isApproved: false, + isPublic: true, + viewCount: 0, + downloadCount: 0, + rating: 0, + ratingCount: 0, + createdAt: now, + updatedAt: now, + }; + + // Create initial version + await db.insert(scriptVersions).values({ + id: generateId(), + scriptId: scriptId, + version: data.version || '1.0.0', + content: data.content, + changelog: 'Initial version', + createdAt: now, + createdBy: data.authorId, + }); + + return script; + } catch (error) { + throw new ApiError(`Failed to create script: ${error}`, 500); + } +} + +// Get script by ID +export async function getScriptById(id: string) { + try { + const script = await db.query.scripts.findFirst({ + where: eq(scripts.id, id), + with: { + author: true, + versions: { + orderBy: desc(scriptVersions.createdAt), + }, + ratings: true, + }, + }); + + if (!script) { + throw new ApiError('Script not found', 404); + } + + return script; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get script: ${error}`, 500); + } +} + +// Get scripts with filters +export async function getScripts(filters: ScriptFilters = {}) { + try { + const { + categories, + compatibleOs, + search, + authorId, + isApproved = true, + sortBy = 'newest', + limit = 20, + offset = 0, + } = filters; + + let query = db.select().from(scripts); + let conditions: any[] = []; + + // Apply filters + if (isApproved !== undefined) { + conditions.push(eq(scripts.isApproved, isApproved)); + } + + if (authorId) { + conditions.push(eq(scripts.authorId, authorId)); + } + + if (search) { + conditions.push( + or( + like(scripts.name, `%${search}%`), + like(scripts.description, `%${search}%`) + ) + ); + } + + if (categories && categories.length > 0) { + conditions.push( + sql`JSON_OVERLAPS(${scripts.categories}, ${JSON.stringify(categories)})` + ); + } + + if (compatibleOs && compatibleOs.length > 0) { + conditions.push( + sql`JSON_OVERLAPS(${scripts.compatibleOs}, ${JSON.stringify(compatibleOs)})` + ); + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any; + } + + // Apply sorting + switch (sortBy) { + case 'newest': + query = query.orderBy(desc(scripts.createdAt)) as any; + break; + case 'oldest': + query = query.orderBy(asc(scripts.createdAt)) as any; + break; + case 'popular': + query = query.orderBy(desc(scripts.viewCount)) as any; + break; + case 'rating': + query = query.orderBy(desc(scripts.rating)) as any; + break; + } + + // Apply pagination + query = query.limit(limit).offset(offset) as any; + + const results = await query; + + // Get total count for pagination + const [{ total }] = await db + .select({ total: count() }) + .from(scripts) + .where(conditions.length > 0 ? and(...conditions) : undefined); + + return { + scripts: results, + total, + hasMore: offset + limit < total, + }; + } catch (error) { + throw new ApiError(`Failed to get scripts: ${error}`, 500); + } +} + +// Update script +export async function updateScript(id: string, data: UpdateScriptData, userId: string) { + try { + // Check if user owns the script or is admin + const script = await getScriptById(id); + if (script.authorId !== userId) { + throw new ApiError('Unauthorized to update this script', 403); + } + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(scripts) + .set(updateData) + .where(eq(scripts.id, id)); + + const updatedScript = { ...script, ...updateData }; + + // If content changed, create new version + if (data.content && data.version) { + await db.insert(scriptVersions).values({ + id: generateId(), + scriptId: id, + version: data.version, + content: data.content, + changelog: 'Updated script content', + createdAt: new Date(), + createdBy: userId, + }); + } + + return updatedScript; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to update script: ${error}`, 500); + } +} + +// Delete script +export async function deleteScript(id: string, userId: string) { + try { + const script = await getScriptById(id); + if (script.authorId !== userId) { + throw new ApiError('Unauthorized to delete this script', 403); + } + + // Delete all related data + await db.delete(scriptVersions).where(eq(scriptVersions.scriptId, id)); + await db.delete(ratings).where(eq(ratings.scriptId, id)); + await db.delete(scripts).where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to delete script: ${error}`, 500); + } +} + +// Approve/reject script (admin only) +export async function moderateScript(id: string, isApproved: boolean, _moderatorId: string) { + try { + const script = await getScriptById(id); + if (!script) { + throw new ApiError('Script not found', 404); + } + + await db + .update(scripts) + .set({ + isApproved, + updatedAt: new Date(), + }) + .where(eq(scripts.id, id)); + + const moderatedScript = { ...script, isApproved, updatedAt: new Date() }; + return moderatedScript; + } catch (error) { + throw new ApiError(`Failed to moderate script: ${error}`, 500); + } +} + +// Increment view count +export async function incrementViewCount(id: string) { + try { + await db + .update(scripts) + .set({ + viewCount: sql`${scripts.viewCount} + 1`, + }) + .where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to increment view count: ${error}`, 500); + } +} + +// Increment download count +export async function incrementDownloadCount(id: string) { + try { + await db + .update(scripts) + .set({ + downloadCount: sql`${scripts.downloadCount} + 1`, + }) + .where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to increment download count: ${error}`, 500); + } +} + +// Get popular scripts +export async function getPopularScripts(limit: number = 10) { + try { + const popularScripts = await db + .select() + .from(scripts) + .where(eq(scripts.isApproved, true)) + .orderBy(desc(scripts.viewCount)) + .limit(limit); + + return popularScripts; + } catch (error) { + throw new ApiError(`Failed to get popular scripts: ${error}`, 500); + } +} + +// Get recent scripts +export async function getRecentScripts(limit: number = 10) { + try { + const recentScripts = await db + .select() + .from(scripts) + .where(eq(scripts.isApproved, true)) + .orderBy(desc(scripts.createdAt)) + .limit(limit); + + return recentScripts; + } catch (error) { + throw new ApiError(`Failed to get recent scripts: ${error}`, 500); + } +} diff --git a/temp_api_backup/api/users.ts b/temp_api_backup/api/users.ts new file mode 100644 index 0000000..7486132 --- /dev/null +++ b/temp_api_backup/api/users.ts @@ -0,0 +1,174 @@ +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { eq, like } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateUserData { + email: string; + username: string; + displayName: string; + avatarUrl?: string; + bio?: string; +} + +export interface UpdateUserData { + username?: string; + displayName?: string; + avatarUrl?: string; + bio?: string; +} + +// Create a new user +export async function createUser(data: CreateUserData) { + try { + const userId = generateId(); + const now = new Date(); + + const userData = { + id: userId, + email: data.email, + username: data.username, + displayName: data.displayName, + avatarUrl: data.avatarUrl || null, + bio: data.bio || null, + isAdmin: false, + isModerator: false, + passwordHash: '', // This should be set by auth layer + createdAt: now, + updatedAt: now, + }; + + await db.insert(users).values(userData); + return userData; + } catch (error) { + throw new ApiError(`Failed to create user: ${error}`, 500); + } +} + +// Get user by ID +export async function getUserById(id: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.id, id), + with: { + scripts: { + where: eq(users.isAdmin, true) ? undefined : eq(users.id, id), // Only show own scripts unless admin + }, + }, + }); + + if (!user) { + throw new ApiError('User not found', 404); + } + + return user; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get user: ${error}`, 500); + } +} + +// Get user by email +export async function getUserByEmail(email: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + return user; + } catch (error) { + throw new ApiError(`Failed to get user by email: ${error}`, 500); + } +} + +// Get user by username +export async function getUserByUsername(username: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.username, username), + }); + + return user; + } catch (error) { + throw new ApiError(`Failed to get user by username: ${error}`, 500); + } +} + +// Update user +export async function updateUser(id: string, data: UpdateUserData) { + try { + const user = await getUserById(id); + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(users) + .set(updateData) + .where(eq(users.id, id)); + + const updatedUser = { ...user, ...updateData }; + return updatedUser; + } catch (error) { + throw new ApiError(`Failed to update user: ${error}`, 500); + } +} + +// Update user permissions (admin only) +export async function updateUserPermissions( + id: string, + permissions: { isAdmin?: boolean; isModerator?: boolean } +) { + try { + const user = await getUserById(id); + + const updateData = { + ...permissions, + updatedAt: new Date(), + }; + + await db + .update(users) + .set(updateData) + .where(eq(users.id, id)); + + const updatedUser = { ...user, ...updateData }; + return updatedUser; + } catch (error) { + throw new ApiError(`Failed to update user permissions: ${error}`, 500); + } +} + +// Search users +export async function searchUsers(query: string, limit: number = 20) { + try { + const searchResults = await db + .select() + .from(users) + .where( + like(users.username, `%${query}%`) + ) + .limit(limit); + + return searchResults; + } catch (error) { + throw new ApiError(`Failed to search users: ${error}`, 500); + } +} + +// Get all users (admin only) +export async function getAllUsers(limit: number = 50, offset: number = 0) { + try { + const allUsers = await db + .select() + .from(users) + .limit(limit) + .offset(offset); + + return allUsers; + } catch (error) { + throw new ApiError(`Failed to get all users: ${error}`, 500); + } +} diff --git a/src/lib/db/browser.ts b/temp_api_backup/db/browser.ts similarity index 100% rename from src/lib/db/browser.ts rename to temp_api_backup/db/browser.ts diff --git a/temp_api_backup/db/index.ts b/temp_api_backup/db/index.ts new file mode 100644 index 0000000..f5cd256 --- /dev/null +++ b/temp_api_backup/db/index.ts @@ -0,0 +1,26 @@ +import { drizzle } from 'drizzle-orm/mysql2'; +import mysql from 'mysql2/promise'; +import * as schema from './schema'; + +// Create the connection pool +const connection = await mysql.createConnection({ + uri: process.env.DATABASE_URL!, +}); + +// Create the drizzle database instance +export const db = drizzle(connection, { schema, mode: 'default' }); + +// Export the schema for use in other parts of the app +export * from './schema'; + +// Test the connection +export const testConnection = async () => { + try { + await connection.ping(); + console.log('✅ Database connection successful'); + return true; + } catch (error) { + console.error('❌ Database connection failed:', error); + return false; + } +}; diff --git a/temp_api_backup/db/schema.ts b/temp_api_backup/db/schema.ts new file mode 100644 index 0000000..0740ea0 --- /dev/null +++ b/temp_api_backup/db/schema.ts @@ -0,0 +1,186 @@ +import { mysqlTable, varchar, text, timestamp, int, boolean, json, index } from 'drizzle-orm/mysql-core'; +import { relations } from 'drizzle-orm'; + +// Users table +export const users = mysqlTable('users', { + id: varchar('id', { length: 255 }).primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + username: varchar('username', { length: 100 }).notNull().unique(), + displayName: varchar('display_name', { length: 100 }).notNull(), + avatarUrl: varchar('avatar_url', { length: 500 }), + bio: text('bio'), + isAdmin: boolean('is_admin').default(false), + isModerator: boolean('is_moderator').default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + emailIdx: index('email_idx').on(table.email), + usernameIdx: index('username_idx').on(table.username), +})); + +// Scripts table +export const scripts = mysqlTable('scripts', { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 200 }).notNull(), + description: text('description').notNull(), + content: text('content').notNull(), + compatibleOs: json('compatible_os').$type().notNull(), + categories: json('categories').$type().notNull(), + tags: json('tags').$type(), + gitRepositoryUrl: varchar('git_repository_url', { length: 500 }), + authorId: varchar('author_id', { length: 255 }).notNull(), + authorName: varchar('author_name', { length: 100 }).notNull(), + viewCount: int('view_count').default(0).notNull(), + downloadCount: int('download_count').default(0).notNull(), + rating: int('rating').default(0).notNull(), + ratingCount: int('rating_count').default(0).notNull(), + isApproved: boolean('is_approved').default(false).notNull(), + isPublic: boolean('is_public').default(true).notNull(), + version: varchar('version', { length: 20 }).default('1.0.0').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + authorIdx: index('author_idx').on(table.authorId), + approvedIdx: index('approved_idx').on(table.isApproved), + publicIdx: index('public_idx').on(table.isPublic), + createdAtIdx: index('created_at_idx').on(table.createdAt), +})); + +// Script versions table +export const scriptVersions = mysqlTable('script_versions', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + version: varchar('version', { length: 20 }).notNull(), + content: text('content').notNull(), + changelog: text('changelog'), + createdAt: timestamp('created_at').defaultNow().notNull(), + createdBy: varchar('created_by', { length: 255 }).notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + versionIdx: index('version_idx').on(table.version), +})); + + + +// Ratings table +export const ratings = mysqlTable('ratings', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + userId: varchar('user_id', { length: 255 }).notNull(), + rating: int('rating').notNull(), // 1-5 stars + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + userIdx: index('user_idx').on(table.userId), + uniqueRating: index('unique_rating').on(table.scriptId, table.userId), +})); + +// Script collections table +export const scriptCollections = mysqlTable('script_collections', { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 200 }).notNull(), + description: text('description'), + authorId: varchar('author_id', { length: 255 }).notNull(), + isPublic: boolean('is_public').default(true).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + authorIdx: index('author_idx').on(table.authorId), + publicIdx: index('public_idx').on(table.isPublic), +})); + +// Collection scripts junction table +export const collectionScripts = mysqlTable('collection_scripts', { + id: varchar('id', { length: 255 }).primaryKey(), + collectionId: varchar('collection_id', { length: 255 }).notNull(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + addedAt: timestamp('added_at').defaultNow().notNull(), +}, (table) => ({ + collectionIdx: index('collection_idx').on(table.collectionId), + scriptIdx: index('script_idx').on(table.scriptId), +})); + +// Script analytics table +export const scriptAnalytics = mysqlTable('script_analytics', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + eventType: varchar('event_type', { length: 50 }).notNull(), // view, download, share + userId: varchar('user_id', { length: 255 }), + userAgent: text('user_agent'), + ipAddress: varchar('ip_address', { length: 45 }), + referrer: varchar('referrer', { length: 500 }), + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + eventIdx: index('event_idx').on(table.eventType), + userIdx: index('user_idx').on(table.userId), + createdAtIdx: index('created_at_idx').on(table.createdAt), +})); + +// Define relationships +export const usersRelations = relations(users, ({ many }) => ({ + scripts: many(scripts), + ratings: many(ratings), + collections: many(scriptCollections), +})); + +export const scriptsRelations = relations(scripts, ({ one, many }) => ({ + author: one(users, { + fields: [scripts.authorId], + references: [users.id], + }), + versions: many(scriptVersions), + ratings: many(ratings), + analytics: many(scriptAnalytics), +})); + +export const scriptVersionsRelations = relations(scriptVersions, ({ one }) => ({ + script: one(scripts, { + fields: [scriptVersions.scriptId], + references: [scripts.id], + }), +})); + + + +export const ratingsRelations = relations(ratings, ({ one }) => ({ + script: one(scripts, { + fields: [ratings.scriptId], + references: [scripts.id], + }), + user: one(users, { + fields: [ratings.userId], + references: [users.id], + }), +})); + +export const scriptCollectionsRelations = relations(scriptCollections, ({ one, many }) => ({ + author: one(users, { + fields: [scriptCollections.authorId], + references: [users.id], + }), + scripts: many(collectionScripts), +})); + +export const collectionScriptsRelations = relations(collectionScripts, ({ one }) => ({ + collection: one(scriptCollections, { + fields: [collectionScripts.collectionId], + references: [scriptCollections.id], + }), + script: one(scripts, { + fields: [collectionScripts.scriptId], + references: [scripts.id], + }), +})); + +export const scriptAnalyticsRelations = relations(scriptAnalytics, ({ one }) => ({ + script: one(scripts, { + fields: [scriptAnalytics.scriptId], + references: [scripts.id], + }), + user: one(users, { + fields: [scriptAnalytics.userId], + references: [users.id], + }), +}));