Skip to main content
Glama
rankings.ts18.4 kB
import { and, asc, desc, eq, like, not, or, sql } from "drizzle-orm"; import { db } from "../../index"; import { apps, rankingRecords, rankings, snapshots, snapshotsWeekly, snapshotsMonthly } from "../../schema"; import { CreateAppSubmission, CreateRankingRecord, formatPeriodKey, RankingApp, RankingType, UpdateRankingRecord, zCreateRankingSchema, zSearchRankingsSchema, zUpdateRankingSchema } from "../../types"; import { slugifyText } from "../../utils/generate-string"; // Rankings data access module export const rankingsDataAccess = { // Create ranking create: async (data: typeof zCreateRankingSchema._type) => { // Check if name already exists if (data.name) { const existingRanking = await db.query.rankings.findFirst({ where: eq(rankings.name, data.name), }); if (existingRanking) { throw new Error(`Ranking name "${data.name}" already exists`); } } return db .insert(rankings) .values({ name: data.name, type: data.type, source: data.source, description: data.description, periodKey: data.periodKey, status: true, }) .returning(); }, // Update ranking update: async (id: string, data: typeof zUpdateRankingSchema._type) => { try { // Check if name already exists if (data.name) { const existingRanking = await db.query.rankings.findFirst({ where: and( eq(rankings.name, data.name), not(eq(rankings.id, id)) ), }); if (existingRanking) { throw new Error(`Ranking name "${data.name}" already exists`); } } // Build update data const updateData: Record<string, any> = {}; if (data.name) updateData.name = data.name; if (data.description !== undefined) updateData.description = data.description; return db.update(rankings).set(updateData).where(eq(rankings.id, id)).returning(); } catch (error) { if (error instanceof Error && error.message.includes("already exists")) { throw error; } throw new Error("Failed to update ranking"); } }, updateStatus: async (id: string, status: boolean) => { return db.update(rankings).set({ status }).where(eq(rankings.id, id)).returning(); }, // Get ranking by ID getById: async (id: string) => { return db.query.rankings.findFirst({ where: eq(rankings.id, id), }); }, // Search rankings search: async (params: typeof zSearchRankingsSchema._type) => { const { query, page = 1, limit = 10, field, order } = params; const offset = (page - 1) * limit; // Build query conditions const conditions = []; if (query) { conditions.push(like(rankings.name, `%${query}%`)); } // Build sort conditions const orderBy = []; if (field) { const orderDirection = order === "desc" ? desc : asc; if (field === "name") orderBy.push(orderDirection(rankings.name)); if (field === "createdAt") orderBy.push(orderDirection(rankings.createdAt)); if (field === "updatedAt") orderBy.push(orderDirection(rankings.updatedAt)); } else { orderBy.push(desc(rankings.createdAt)); } // Execute query const results = await db.query.rankings.findMany({ where: conditions.length > 0 ? and(...conditions) : undefined, orderBy, limit, offset, }); // Get total count const countResult = await db .select({ count: sql<number>`count(*)` }) .from(rankings) .where(conditions.length > 0 ? and(...conditions) : undefined); const total = countResult[0]?.count ?? 0; return { data: results, pagination: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; }, list: async (params: typeof zSearchRankingsSchema._type) => { const { query, page = 1, limit = 10, field, order } = params; const offset = (page - 1) * limit; const conditions = []; if (query) { conditions.push(like(rankings.name, `%${query}%`)); } const orderBy = []; if (field) { const orderDirection = order === "desc" ? desc : asc; if (field === "name") orderBy.push(orderDirection(rankings.name)); if (field === "createdAt") orderBy.push(orderDirection(rankings.createdAt)); if (field === "updatedAt") orderBy.push(orderDirection(rankings.updatedAt)); } else { orderBy.push(desc(rankings.createdAt)); } const results = await db.query.rankings.findMany({ where: conditions.length > 0 ? and(...conditions) : undefined, orderBy, limit, offset, }); return { data: results, pagination: { page, limit, }, }; }, getRankingAppsByRankingId: async (rankingId: string, limit?: number): Promise<RankingApp[]> => { const rankingRecordsWithApps = await db.query.rankingRecords.findMany({ where: eq(rankingRecords.rankingId, rankingId), with: { app: { with: { tags: { with: { tag: true } } } }, }, orderBy: desc(rankingRecords.rank), limit: limit ? limit : 10, }); return rankingRecordsWithApps.map(record => ({ id: record.app.id, name: record.app.name, slug: record.app.slug, description: record.app.description, descriptionZh: record.app.descriptionZh || undefined, icon: record.app.icon || "", stars: record.app.stars || 0, watchers: record.app.watchers || 0, forks: record.app.forks || 0, primaryLanguage: record.app.primaryLanguage || "", languages: record.app.languages || [], license: record.app.license || "", tags: record.app.tags.map(t => ({ id: t.tag.id, name: t.tag.name, slug: t.tag.slug, description: t.tag.description || "", type: t.tag.type, source: t.tag.source, totalApps: t.tag.totalApps || 0, createdAt: t.tag.createdAt, })), type: record.app.type, source: record.app.source, status: record.app.status, rank: record.rank || 0, createdAt: record.app.createdAt, })); }, findOne: async (data: { type: "daily" | "weekly" | "monthly" | "yearly"; source: "github" | "openmcp" | "producthunt"; periodKey: string }) => { return db.query.rankings.findFirst({ where: and( eq(rankings.type, data.type), eq(rankings.source, data.source), eq(rankings.periodKey, data.periodKey), eq(rankings.status, true) ), }); }, /** * 从快照数据计算日排行 */ calculateDailyRankingFromSnapshots: async (date: Date) => { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); // 获取指定日期的快照数据,并关联应用数据 const snapshotData = await db .select({ snapshot: snapshots, app: apps, }) .from(snapshots) .innerJoin(apps, eq(snapshots.repoId, apps.repoId)) .where(and( eq(snapshots.year, year), eq(snapshots.month, month), eq(snapshots.day, day) )) .orderBy(desc(snapshots.stars)) .limit(30); if (snapshotData.length === 0) { return null; } // 创建或更新排行 const periodKey = formatPeriodKey('daily', date); const rankingName = `Github 日排行 ${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const [ranking] = await db.insert(rankings).values({ name: rankingName, type: 'daily', source: 'github', description: `Github Daily Rank ${periodKey}`, periodKey, status: true, }).onConflictDoUpdate({ target: [rankings.type, rankings.source, rankings.periodKey], set: { name: rankingName, updatedAt: new Date(), }, }).returning(); if (!ranking) { throw new Error("Failed to create or update ranking"); } // 创建排行记录 const records = snapshotData .filter(item => item.app) // 过滤掉没有应用的数据 .map((item, index) => ({ rankingId: ranking.id, entityId: item.app!.id, entityName: item.app!.name, entityType: "apps" as const, score: item.snapshot.stars || 0, rank: index + 1, })); if (records.length > 0) { await db.insert(rankingRecords).values(records).onConflictDoNothing(); } return ranking; }, /** * 从快照数据计算周排行 */ calculateWeeklyRankingFromSnapshots: async (date: Date) => { const year = date.getFullYear(); const week = Math.ceil((date.getTime() - new Date(year, 0, 1).getTime()) / (7 * 24 * 60 * 60 * 1000)); // 获取指定周的周快照数据,并关联应用数据 const snapshotData = await db .select({ snapshot: snapshotsWeekly, app: { id: apps.id, name: apps.name }, }) .from(snapshotsWeekly) .innerJoin(apps, eq(snapshotsWeekly.repoId, apps.repoId)) .where(and( eq(snapshotsWeekly.year, year), eq(snapshotsWeekly.week, week) )) .orderBy(desc(snapshotsWeekly.stars)) .limit(30); if (snapshotData.length === 0) { return null; } // 创建或更新排行 const periodKey = formatPeriodKey('weekly', date); const rankingName = `Github 周排行 ${year}-W${week}`; const [ranking] = await db.insert(rankings).values({ name: rankingName, type: 'weekly', source: 'github', description: `Github Weekly Rank ${periodKey}`, periodKey, status: true, }).onConflictDoUpdate({ target: [rankings.type, rankings.source, rankings.periodKey], set: { name: rankingName, updatedAt: new Date(), }, }).returning(); if (!ranking) { throw new Error("Failed to create or update ranking"); } // 创建排行记录 const records = snapshotData .filter(item => item.app) // 过滤掉没有应用的数据 .map((item, index) => ({ rankingId: ranking.id, entityId: item.app!.id, entityName: item.app!.name, entityType: "apps" as const, score: item.snapshot.stars || 0, rank: index + 1, })); if (records.length > 0) { await db.insert(rankingRecords).values(records).onConflictDoNothing(); } return ranking; }, /** * 从快照数据计算月排行 */ calculateMonthlyRankingFromSnapshots: async (date: Date) => { const year = date.getFullYear(); const month = date.getMonth() + 1; // 获取指定月份的月快照数据,并关联应用数据 const snapshotData = await db .select({ snapshot: snapshotsMonthly, app: apps, }) .from(snapshotsMonthly) .innerJoin(apps, eq(snapshotsMonthly.repoId, apps.repoId)) .where(and( eq(snapshotsMonthly.year, year), eq(snapshotsMonthly.month, month) )) .orderBy(desc(snapshotsMonthly.stars)) .limit(30); if (snapshotData.length === 0) { return null; } // 创建或更新排行 const periodKey = formatPeriodKey('monthly', date); const rankingName = `Github 月排行 ${year}-${String(month).padStart(2, '0')}`; const [ranking] = await db.insert(rankings).values({ name: rankingName, type: 'monthly', source: 'github', description: `Github Monthly Rank ${periodKey}`, periodKey, status: true, }).onConflictDoUpdate({ target: [rankings.type, rankings.source, rankings.periodKey], set: { name: rankingName, updatedAt: new Date(), }, }).returning(); if (!ranking) { throw new Error("Failed to create or update ranking"); } // 创建排行记录 const records = snapshotData .filter(item => item.app) // 过滤掉没有应用的数据 .map((item, index) => ({ rankingId: ranking.id, entityId: item.app!.id, entityName: item.app!.name, entityType: "apps" as const, score: item.snapshot.stars || 0, rank: index + 1, })); if (records.length > 0) { await db.insert(rankingRecords).values(records).onConflictDoNothing(); } return ranking; }, /** * 根据github的日排行,创建排行榜。 * 首先创建apps,然后创建排行榜,最后创建排行榜记录 * @param data */ createGithubAppSubmissionRank: async (data: CreateAppSubmission[], rankingType: RankingType) => { const rankingId = await db.transaction(async (tx) => { console.info("[api] [db] [rankings.ts] [createGithubAppSubmissionRank] data", data.map(item => ({ github: item.github })) ); const appDatas = data.map(item => ({ name: item.name, slug: slugifyText(item.name as string), description: item.description ?? "", longDescription: item.longDescription, icon: item.favicon, banner: item.logo, type: item.type, docs: item.docs, website: item.website && item.website?.trim().length > 0 ? item.website : null, github: item.github, source: "automatic" as const, status: "approved" as const, publishStatus: "online" as const, userId: item.userId, })); let allAppIds = [] for (const app of appDatas) { const existing = await tx.query.apps.findFirst({ where: or( and(eq(apps.name, app.name), eq(apps.type, app.type)), app.website ? eq(apps.website, app.website) : undefined, app.github ? eq(apps.github, app.github) : undefined ), }); console.info("[api] [db] [rankings.ts] [createGithubAppSubmissionRank] existing", existing ? existing.github : "not found") if (existing) { allAppIds.push(existing); } else { const [newApp] = await tx.insert(apps).values(app).returning(); console.info("[api] [db] [rankings.ts] [createGithubAppSubmissionRank] newApp", newApp ? newApp.github : "not found") if (newApp) { allAppIds.push(newApp); } } } // console.info("[api] [db] [rankings.ts] [createGithubAppSubmissionRank] allAppIds", allAppIds) // 构建排行榜数据 const rankingData = { name: `Github ${rankingType === "daily" ? "日" : rankingType === "weekly" ? "周" : rankingType === "monthly" ? "月" : "年"}排行`, type: rankingType, source: "github" as const, description: `Github ${rankingType} Rank`, periodKey: formatPeriodKey(rankingType, new Date()), status: true, }; const [ranking] = await tx.insert(rankings).values(rankingData).onConflictDoUpdate({ target: [rankings.type, rankings.source, rankings.periodKey], set: { updatedAt: new Date(), }, }).returning(); if (!ranking) { throw new Error("Failed to create or find ranking"); } // 创建排行榜记录 const records = allAppIds?.map((item, index) => ({ rankingId: ranking.id, entityId: item?.id, entityName: item.name, entityType: "apps" as const, score: item?.stars || 0, link: "/apps/" + item?.slug, rank: index + 1, })); await tx.insert(rankingRecords).values(records).onConflictDoNothing(); return allAppIds; }); return rankingId; } }; // Ranking Records data access module export const rankingRecordsDataAccess = { // Create ranking record create: async (data: CreateRankingRecord) => { return db .insert(rankingRecords) .values({ rankingId: data.rankingId, entityId: data.entityId, entityName: data.entityName ?? data.entityId, entityType: data.entityType, score: data.score, rank: data.rank, }) .returning(); }, // Update ranking record update: async (data: UpdateRankingRecord) => { const { id, score, rank } = data; return db .update(rankingRecords) .set({ score, rank, updatedAt: new Date(), }) .where(eq(rankingRecords.id, id)) .returning(); }, // Delete ranking record delete: async (id: string) => { return db .delete(rankingRecords) .where(eq(rankingRecords.id, id)) .returning(); }, // Get ranking record by ID getById: async (id: string) => { return db.query.rankingRecords.findFirst({ where: eq(rankingRecords.id, id), }); }, // Get ranking records by ranking ID getByRankingId: async (rankingId: string) => { return db.query.rankingRecords.findMany({ where: eq(rankingRecords.rankingId, rankingId), orderBy: desc(rankingRecords.rank), }); }, // Batch create ranking records batchCreate: async (rankingId: string, records: CreateRankingRecord[]) => { return await db.transaction(async (tx) => { const [count] = await tx.select({ count: sql<number>`count(*)` }).from(rankingRecords).where(eq(rankingRecords.rankingId, rankingId)); // 更新 rankings 表的记录数和更新时间 await tx.update(rankings) .set({ updatedAt: new Date(), recordsCount: records.length + (count ? count.count : 0), }) .where(eq(rankings.id, rankingId)); // 插入新的排行榜记录 const newRecords = await tx .insert(rankingRecords) .values(records.map(record => ({ ...record, entityName: record.entityName || record.entityId }))).onConflictDoNothing() .returning(); return newRecords; }); }, // Batch update ranking records batchUpdate: async (updates: UpdateRankingRecord[]) => { const results = []; for (const update of updates) { const result = await rankingRecordsDataAccess.update(update); results.push(...result); } return results; }, // Delete all records for a ranking deleteByRankingId: async (rankingId: string) => { return db .delete(rankingRecords) .where(eq(rankingRecords.rankingId, rankingId)) .returning(); } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/metacode0602/open-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server