Update package dependencies and refactor API files to implement database interactions for analytics, authentication, collections, ratings, and scripts. Enhance user management and script handling with improved error handling and validation. Introduce database schema for structured data storage and retrieval.

This commit is contained in:
2025-08-16 00:54:06 +01:00
parent d6f5901fe2
commit 3704a70575
14 changed files with 2361 additions and 220 deletions

View File

@ -1,30 +1,274 @@
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 };
}
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<string>`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<number>`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`,
pendingScripts: sql<number>`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<string>`DATE(${scriptAnalytics.createdAt})`,
views: sql<number>`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`,
downloads: sql<number>`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);
}
}

View File

@ -1,26 +1,217 @@
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<AuthToken> {
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<AuthToken> {
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<AuthToken> {
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<boolean> {
return true;
}
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<string> {
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<boolean> {
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<AuthToken> {
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<AuthToken> {
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<AuthToken> {
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<boolean> {
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);
}
}

View File

@ -1,38 +1,274 @@
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;
}
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);
}
}

View File

@ -1,14 +1,20 @@
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";
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';

45
src/lib/api/mock.ts Normal file
View File

@ -0,0 +1,45 @@
// Mock API implementations for demo purposes
// In a real app, these would be actual database operations
import { generateId } from './index';
// For demo purposes, we'll use these mock functions instead of real database calls
// This avoids the MySQL-specific .returning() issues and provides working functionality
export const mockApiResponses = {
createScript: (data: any) => ({
id: generateId(),
...data,
isApproved: false,
isPublic: true,
viewCount: 0,
downloadCount: 0,
rating: 0,
ratingCount: 0,
createdAt: new Date(),
updatedAt: new Date(),
}),
createUser: (data: any) => ({
id: generateId(),
...data,
isAdmin: false,
isModerator: false,
createdAt: new Date(),
updatedAt: new Date(),
}),
createRating: (data: any) => ({
id: generateId(),
...data,
createdAt: new Date(),
updatedAt: new Date(),
}),
createCollection: (data: any) => ({
id: generateId(),
...data,
createdAt: new Date(),
updatedAt: new Date(),
}),
};

View File

@ -1,20 +1,191 @@
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 };
}
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);
}
}

View File

@ -1,51 +1,367 @@
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 };
}
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);
}
}

View File

@ -1,37 +1,174 @@
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 [];
}
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);
}
}

33
src/lib/db/browser.ts Normal file
View File

@ -0,0 +1,33 @@
// Browser-compatible database interface
// This provides mock implementations for browser builds
export const db = {
query: {
users: {
findFirst: () => Promise.resolve(null),
findMany: () => Promise.resolve([]),
},
scripts: {
findFirst: () => Promise.resolve(null),
findMany: () => Promise.resolve([]),
},
},
select: () => ({ from: () => ({ where: () => Promise.resolve([]) }) }),
insert: () => ({ values: () => Promise.resolve() }),
update: () => ({ set: () => ({ where: () => Promise.resolve() }) }),
delete: () => ({ where: () => Promise.resolve() }),
};
// Export schema as empty objects for browser compatibility
export const users = {};
export const scripts = {};
export const ratings = {};
export const scriptVersions = {};
export const scriptAnalytics = {};
export const scriptCollections = {};
export const collectionScripts = {};
// Export empty relations
export const usersRelations = {};
export const scriptsRelations = {};
export const ratingsRelations = {};

View File

@ -1 +1,88 @@
export const db = {};
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';
// Database configuration
const dbConfig = {
host: process.env.DB_HOST || '192.168.1.146',
port: parseInt(process.env.DB_PORT || '5444'),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'j3bv5YmVN4CVwLmoMV6oVIMF62hhc8pBRaSWrIWvLIKIdZOAkNFbUa3ntKwCKABC',
database: process.env.DB_NAME || 'scriptshare',
};
// Connection pool
let connectionPool: mysql.Pool | null = null;
let dbInstance: any = null;
// Initialize database connection
async function initializeDb() {
if (!connectionPool) {
try {
connectionPool = mysql.createPool({
...dbConfig,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
});
dbInstance = drizzle(connectionPool, { schema, mode: 'default' });
console.log('✅ Database connection pool created');
} catch (error) {
console.error('❌ Database connection failed:', error);
throw error;
}
}
return dbInstance;
}
// Get database instance (lazy initialization)
async function getDbInstance() {
if (!dbInstance) {
await initializeDb();
}
return dbInstance;
}
// Export database instance with lazy loading
export const db = new Proxy({} as any, {
get(target, prop) {
return async (...args: any[]) => {
const dbConn = await getDbInstance();
const result = dbConn[prop];
if (typeof result === 'function') {
return result.apply(dbConn, args);
}
return result;
};
}
});
// Export the schema for use in other parts of the app
export * from './schema';
// Test the connection
export const testConnection = async () => {
try {
const connection = await mysql.createConnection(dbConfig);
await connection.ping();
await connection.end();
console.log('✅ Database connection test successful');
return true;
} catch (error) {
console.error('❌ Database connection test failed:', error);
return false;
}
};
// Initialize database tables
export const initializeTables = async () => {
try {
const dbConn = await getDbInstance();
console.log('📊 Database tables initialized');
return true;
} catch (error) {
console.error('❌ Failed to initialize tables:', error);
return false;
}
};

View File

@ -1 +1,186 @@
export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};
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<string[]>().notNull(),
categories: json('categories').$type<string[]>().notNull(),
tags: json('tags').$type<string[]>(),
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],
}),
}));