705 lines
20 KiB
TypeScript
705 lines
20 KiB
TypeScript
import { eq, desc, and, like, or, sql, isNull, ne, asc } from "drizzle-orm";
|
|
import { drizzle } from "drizzle-orm/mysql2";
|
|
import {
|
|
InsertUser, users,
|
|
categories, InsertCategory,
|
|
websites, InsertWebsite,
|
|
products, InsertProduct,
|
|
productPrices, InsertProductPrice,
|
|
bounties, InsertBounty,
|
|
bountyApplications, InsertBountyApplication,
|
|
bountyComments, InsertBountyComment,
|
|
notifications, InsertNotification,
|
|
favorites, InsertFavorite,
|
|
favoriteTags, InsertFavoriteTag,
|
|
favoriteTagMappings, InsertFavoriteTagMapping,
|
|
priceMonitors, InsertPriceMonitor,
|
|
priceHistory, InsertPriceHistory
|
|
} from "../drizzle/schema";
|
|
import { ENV } from './_core/env';
|
|
|
|
let _db: ReturnType<typeof drizzle> | null = null;
|
|
|
|
export async function getDb() {
|
|
if (!_db && process.env.DATABASE_URL) {
|
|
try {
|
|
_db = drizzle(process.env.DATABASE_URL);
|
|
} catch (error) {
|
|
console.warn("[Database] Failed to connect:", error);
|
|
_db = null;
|
|
}
|
|
}
|
|
return _db;
|
|
}
|
|
|
|
// ============ User Functions ============
|
|
export async function upsertUser(user: InsertUser): Promise<void> {
|
|
if (!user.openId) {
|
|
throw new Error("User openId is required for upsert");
|
|
}
|
|
|
|
const db = await getDb();
|
|
if (!db) {
|
|
console.warn("[Database] Cannot upsert user: database not available");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const values: InsertUser = {
|
|
openId: user.openId,
|
|
};
|
|
const updateSet: Record<string, unknown> = {};
|
|
|
|
const textFields = ["name", "email", "loginMethod", "avatar"] as const;
|
|
type TextField = (typeof textFields)[number];
|
|
|
|
const assignNullable = (field: TextField) => {
|
|
const value = user[field];
|
|
if (value === undefined) return;
|
|
const normalized = value ?? null;
|
|
values[field] = normalized;
|
|
updateSet[field] = normalized;
|
|
};
|
|
|
|
textFields.forEach(assignNullable);
|
|
|
|
if (user.lastSignedIn !== undefined) {
|
|
values.lastSignedIn = user.lastSignedIn;
|
|
updateSet.lastSignedIn = user.lastSignedIn;
|
|
}
|
|
if (user.role !== undefined) {
|
|
values.role = user.role;
|
|
updateSet.role = user.role;
|
|
} else if (user.openId === ENV.ownerOpenId) {
|
|
values.role = 'admin';
|
|
updateSet.role = 'admin';
|
|
}
|
|
|
|
if (!values.lastSignedIn) {
|
|
values.lastSignedIn = new Date();
|
|
}
|
|
|
|
if (Object.keys(updateSet).length === 0) {
|
|
updateSet.lastSignedIn = new Date();
|
|
}
|
|
|
|
await db.insert(users).values(values).onDuplicateKeyUpdate({
|
|
set: updateSet,
|
|
});
|
|
} catch (error) {
|
|
console.error("[Database] Failed to upsert user:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function getUserByOpenId(openId: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function getUserById(id: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function updateUserStripeInfo(userId: number, stripeCustomerId?: string, stripeAccountId?: string) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
const updateData: Record<string, unknown> = {};
|
|
if (stripeCustomerId) updateData.stripeCustomerId = stripeCustomerId;
|
|
if (stripeAccountId) updateData.stripeAccountId = stripeAccountId;
|
|
if (Object.keys(updateData).length > 0) {
|
|
await db.update(users).set(updateData).where(eq(users.id, userId));
|
|
}
|
|
}
|
|
|
|
// ============ Category Functions ============
|
|
export async function getAllCategories() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(categories).orderBy(categories.sortOrder);
|
|
}
|
|
|
|
export async function getCategoryBySlug(slug: string) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(categories).where(eq(categories.slug, slug)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function createCategory(data: InsertCategory) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(categories).values(data);
|
|
}
|
|
|
|
// ============ Website Functions ============
|
|
export async function getWebsites(categoryId?: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
if (categoryId) {
|
|
return db.select().from(websites).where(eq(websites.categoryId, categoryId)).orderBy(websites.sortOrder);
|
|
}
|
|
return db.select().from(websites).orderBy(websites.sortOrder);
|
|
}
|
|
|
|
export async function getWebsiteById(id: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(websites).where(eq(websites.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function createWebsite(data: InsertWebsite) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(websites).values(data);
|
|
}
|
|
|
|
export async function searchWebsites(query: string) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(websites).where(
|
|
or(
|
|
like(websites.name, `%${query}%`),
|
|
like(websites.description, `%${query}%`)
|
|
)
|
|
);
|
|
}
|
|
|
|
// ============ Product Functions ============
|
|
export async function getProducts(
|
|
categoryId?: number,
|
|
limit = 50,
|
|
offset = 0,
|
|
sortBy?: 'price_asc' | 'price_desc' | 'newest' | 'oldest'
|
|
) {
|
|
const database = await getDb();
|
|
if (!database) return [];
|
|
|
|
let query: any = database.select().from(products);
|
|
|
|
if (categoryId) {
|
|
query = query.where(eq(products.categoryId, categoryId));
|
|
}
|
|
|
|
// Apply sorting
|
|
if (sortBy === 'newest') {
|
|
query = query.orderBy(desc(products.createdAt));
|
|
} else if (sortBy === 'oldest') {
|
|
query = query.orderBy(asc(products.createdAt));
|
|
} else {
|
|
// Default: newest first
|
|
query = query.orderBy(desc(products.createdAt));
|
|
}
|
|
|
|
// Apply pagination
|
|
return query.limit(limit).offset(offset);
|
|
}
|
|
|
|
export async function getProductById(id: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(products).where(eq(products.id, id)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function createProduct(data: InsertProduct) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(products).values(data);
|
|
}
|
|
|
|
export async function searchProducts(query: string) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(products).where(
|
|
or(
|
|
like(products.name, `%${query}%`),
|
|
like(products.description, `%${query}%`)
|
|
)
|
|
);
|
|
}
|
|
|
|
// ============ Product Price Functions ============
|
|
export async function getProductPrices(productId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(productPrices).where(eq(productPrices.productId, productId));
|
|
}
|
|
|
|
export async function createProductPrice(data: InsertProductPrice) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(productPrices).values(data);
|
|
}
|
|
|
|
export async function getProductWithPrices(productId: number) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
const product = await getProductById(productId);
|
|
if (!product) return null;
|
|
|
|
const prices = await db.select({
|
|
price: productPrices,
|
|
website: websites
|
|
}).from(productPrices)
|
|
.leftJoin(websites, eq(productPrices.websiteId, websites.id))
|
|
.where(eq(productPrices.productId, productId))
|
|
.orderBy(productPrices.price);
|
|
|
|
return { product, prices };
|
|
}
|
|
|
|
// ============ Bounty Functions ============
|
|
export async function getBounties(status?: string, limit = 50, offset = 0) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
const baseQuery = db.select({
|
|
bounty: bounties,
|
|
publisher: {
|
|
id: users.id,
|
|
name: users.name,
|
|
avatar: users.avatar
|
|
}
|
|
}).from(bounties)
|
|
.leftJoin(users, eq(bounties.publisherId, users.id))
|
|
.orderBy(desc(bounties.createdAt))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
|
|
if (status) {
|
|
return baseQuery.where(eq(bounties.status, status as any));
|
|
}
|
|
return baseQuery;
|
|
}
|
|
|
|
export async function getBountyById(id: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
|
|
const result = await db.select({
|
|
bounty: bounties,
|
|
publisher: {
|
|
id: users.id,
|
|
name: users.name,
|
|
avatar: users.avatar
|
|
}
|
|
}).from(bounties)
|
|
.leftJoin(users, eq(bounties.publisherId, users.id))
|
|
.where(eq(bounties.id, id))
|
|
.limit(1);
|
|
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
export async function createBounty(data: InsertBounty) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
const result = await db.insert(bounties).values(data);
|
|
return result[0].insertId;
|
|
}
|
|
|
|
export async function updateBounty(id: number, data: Partial<InsertBounty>) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(bounties).set(data).where(eq(bounties.id, id));
|
|
}
|
|
|
|
export async function getUserBounties(userId: number, type: 'published' | 'accepted') {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
if (type === 'published') {
|
|
return db.select().from(bounties).where(eq(bounties.publisherId, userId)).orderBy(desc(bounties.createdAt));
|
|
} else {
|
|
return db.select().from(bounties).where(eq(bounties.acceptorId, userId)).orderBy(desc(bounties.createdAt));
|
|
}
|
|
}
|
|
|
|
export async function searchBounties(query: string) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select({
|
|
bounty: bounties,
|
|
publisher: {
|
|
id: users.id,
|
|
name: users.name,
|
|
avatar: users.avatar
|
|
}
|
|
}).from(bounties)
|
|
.leftJoin(users, eq(bounties.publisherId, users.id))
|
|
.where(
|
|
or(
|
|
like(bounties.title, `%${query}%`),
|
|
like(bounties.description, `%${query}%`)
|
|
)
|
|
)
|
|
.orderBy(desc(bounties.createdAt));
|
|
}
|
|
|
|
// ============ Bounty Application Functions ============
|
|
export async function getBountyApplications(bountyId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return db.select({
|
|
application: bountyApplications,
|
|
applicant: {
|
|
id: users.id,
|
|
name: users.name,
|
|
avatar: users.avatar
|
|
}
|
|
}).from(bountyApplications)
|
|
.leftJoin(users, eq(bountyApplications.applicantId, users.id))
|
|
.where(eq(bountyApplications.bountyId, bountyId))
|
|
.orderBy(desc(bountyApplications.createdAt));
|
|
}
|
|
|
|
export async function createBountyApplication(data: InsertBountyApplication) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(bountyApplications).values(data);
|
|
}
|
|
|
|
export async function updateBountyApplication(id: number, status: 'pending' | 'accepted' | 'rejected') {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(bountyApplications).set({ status }).where(eq(bountyApplications.id, id));
|
|
}
|
|
|
|
export async function getUserApplication(bountyId: number, userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return undefined;
|
|
const result = await db.select().from(bountyApplications)
|
|
.where(and(
|
|
eq(bountyApplications.bountyId, bountyId),
|
|
eq(bountyApplications.applicantId, userId)
|
|
)).limit(1);
|
|
return result.length > 0 ? result[0] : undefined;
|
|
}
|
|
|
|
// ============ Bounty Comment Functions ============
|
|
export async function getBountyComments(bountyId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
|
|
return db.select({
|
|
comment: bountyComments,
|
|
user: {
|
|
id: users.id,
|
|
name: users.name,
|
|
avatar: users.avatar
|
|
}
|
|
}).from(bountyComments)
|
|
.leftJoin(users, eq(bountyComments.userId, users.id))
|
|
.where(eq(bountyComments.bountyId, bountyId))
|
|
.orderBy(bountyComments.createdAt);
|
|
}
|
|
|
|
export async function createBountyComment(data: InsertBountyComment) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(bountyComments).values(data);
|
|
}
|
|
|
|
// ============ Notification Functions ============
|
|
export async function getUserNotifications(userId: number, limit = 50) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(notifications)
|
|
.where(eq(notifications.userId, userId))
|
|
.orderBy(desc(notifications.createdAt))
|
|
.limit(limit);
|
|
}
|
|
|
|
export async function getUnreadNotificationCount(userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return 0;
|
|
const result = await db.select({ count: sql<number>`count(*)` })
|
|
.from(notifications)
|
|
.where(and(
|
|
eq(notifications.userId, userId),
|
|
eq(notifications.isRead, false)
|
|
));
|
|
return result[0]?.count ?? 0;
|
|
}
|
|
|
|
export async function createNotification(data: InsertNotification) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(notifications).values(data);
|
|
}
|
|
|
|
export async function markNotificationAsRead(id: number, userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(notifications)
|
|
.set({ isRead: true })
|
|
.where(and(
|
|
eq(notifications.id, id),
|
|
eq(notifications.userId, userId)
|
|
));
|
|
}
|
|
|
|
export async function markAllNotificationsAsRead(userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(notifications)
|
|
.set({ isRead: true })
|
|
.where(eq(notifications.userId, userId));
|
|
}
|
|
|
|
// ============ Favorite Functions ============
|
|
export async function getUserFavorites(userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select({
|
|
id: favorites.id,
|
|
productId: favorites.productId,
|
|
websiteId: favorites.websiteId,
|
|
createdAt: favorites.createdAt,
|
|
product: {
|
|
id: products.id,
|
|
name: products.name,
|
|
description: products.description,
|
|
image: products.image,
|
|
},
|
|
website: {
|
|
id: websites.id,
|
|
name: websites.name,
|
|
url: websites.url,
|
|
logo: websites.logo,
|
|
}
|
|
})
|
|
.from(favorites)
|
|
.innerJoin(products, eq(favorites.productId, products.id))
|
|
.innerJoin(websites, eq(favorites.websiteId, websites.id))
|
|
.where(eq(favorites.userId, userId))
|
|
.orderBy(desc(favorites.createdAt));
|
|
}
|
|
|
|
export async function isFavorited(userId: number, productId: number, websiteId: number) {
|
|
const db = await getDb();
|
|
if (!db) return false;
|
|
const result = await db.select({ id: favorites.id })
|
|
.from(favorites)
|
|
.where(and(
|
|
eq(favorites.userId, userId),
|
|
eq(favorites.productId, productId),
|
|
eq(favorites.websiteId, websiteId)
|
|
))
|
|
.limit(1);
|
|
return result.length > 0;
|
|
}
|
|
|
|
export async function addFavorite(userId: number, productId: number, websiteId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(favorites).values({
|
|
userId,
|
|
productId,
|
|
websiteId,
|
|
});
|
|
}
|
|
|
|
export async function removeFavorite(userId: number, productId: number, websiteId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.delete(favorites)
|
|
.where(and(
|
|
eq(favorites.userId, userId),
|
|
eq(favorites.productId, productId),
|
|
eq(favorites.websiteId, websiteId)
|
|
));
|
|
}
|
|
|
|
// ============ Favorite Tag Functions ============
|
|
export async function getUserFavoriteTags(userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(favoriteTags)
|
|
.where(eq(favoriteTags.userId, userId))
|
|
.orderBy(desc(favoriteTags.createdAt));
|
|
}
|
|
|
|
export async function createFavoriteTag(userId: number, name: string, color?: string, description?: string) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(favoriteTags).values({
|
|
userId,
|
|
name,
|
|
color: color || '#6366f1',
|
|
description,
|
|
});
|
|
}
|
|
|
|
export async function updateFavoriteTag(id: number, userId: number, data: Partial<InsertFavoriteTag>) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(favoriteTags)
|
|
.set(data)
|
|
.where(and(
|
|
eq(favoriteTags.id, id),
|
|
eq(favoriteTags.userId, userId)
|
|
));
|
|
}
|
|
|
|
export async function deleteFavoriteTag(id: number, userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.delete(favoriteTags)
|
|
.where(and(
|
|
eq(favoriteTags.id, id),
|
|
eq(favoriteTags.userId, userId)
|
|
));
|
|
}
|
|
|
|
export async function addTagToFavorite(favoriteId: number, tagId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
const exists = await db.select({ id: favoriteTagMappings.id })
|
|
.from(favoriteTagMappings)
|
|
.where(and(
|
|
eq(favoriteTagMappings.favoriteId, favoriteId),
|
|
eq(favoriteTagMappings.tagId, tagId)
|
|
))
|
|
.limit(1);
|
|
|
|
if (exists.length === 0) {
|
|
await db.insert(favoriteTagMappings).values({
|
|
favoriteId,
|
|
tagId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function removeTagFromFavorite(favoriteId: number, tagId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.delete(favoriteTagMappings)
|
|
.where(and(
|
|
eq(favoriteTagMappings.favoriteId, favoriteId),
|
|
eq(favoriteTagMappings.tagId, tagId)
|
|
));
|
|
}
|
|
|
|
export async function getFavoritesByTag(userId: number, tagId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select({
|
|
id: favorites.id,
|
|
productId: favorites.productId,
|
|
websiteId: favorites.websiteId,
|
|
createdAt: favorites.createdAt,
|
|
product: {
|
|
id: products.id,
|
|
name: products.name,
|
|
description: products.description,
|
|
image: products.image,
|
|
},
|
|
website: {
|
|
id: websites.id,
|
|
name: websites.name,
|
|
url: websites.url,
|
|
logo: websites.logo,
|
|
}
|
|
})
|
|
.from(favorites)
|
|
.innerJoin(products, eq(favorites.productId, products.id))
|
|
.innerJoin(websites, eq(favorites.websiteId, websites.id))
|
|
.innerJoin(favoriteTagMappings, eq(favorites.id, favoriteTagMappings.favoriteId))
|
|
.where(and(
|
|
eq(favorites.userId, userId),
|
|
eq(favoriteTagMappings.tagId, tagId)
|
|
))
|
|
.orderBy(desc(favorites.createdAt));
|
|
}
|
|
|
|
// ============ Price Monitor Functions ============
|
|
export async function createPriceMonitor(userId: number, favoriteId: number, currentPrice?: number, targetPrice?: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.insert(priceMonitors).values({
|
|
userId,
|
|
favoriteId,
|
|
currentPrice: currentPrice ? currentPrice.toString() : undefined,
|
|
targetPrice: targetPrice ? targetPrice.toString() : undefined,
|
|
lowestPrice: currentPrice ? currentPrice.toString() : undefined,
|
|
highestPrice: currentPrice ? currentPrice.toString() : undefined,
|
|
isActive: true,
|
|
});
|
|
}
|
|
|
|
export async function getPriceMonitor(userId: number, favoriteId: number) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
const result = await db.select().from(priceMonitors)
|
|
.where(and(
|
|
eq(priceMonitors.userId, userId),
|
|
eq(priceMonitors.favoriteId, favoriteId)
|
|
))
|
|
.limit(1);
|
|
return result.length > 0 ? result[0] : null;
|
|
}
|
|
|
|
export async function getUserPriceMonitors(userId: number) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(priceMonitors)
|
|
.where(eq(priceMonitors.userId, userId))
|
|
.orderBy(desc(priceMonitors.createdAt));
|
|
}
|
|
|
|
export async function updatePriceMonitor(monitorId: number, data: Partial<InsertPriceMonitor>) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.update(priceMonitors)
|
|
.set(data)
|
|
.where(eq(priceMonitors.id, monitorId));
|
|
}
|
|
|
|
export async function deletePriceMonitor(monitorId: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
await db.delete(priceMonitors)
|
|
.where(eq(priceMonitors.id, monitorId));
|
|
}
|
|
|
|
export async function recordPriceHistory(monitorId: number, price: number, previousPrice?: number) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
const priceChange = previousPrice ? price - previousPrice : 0;
|
|
const percentChange = previousPrice ? ((priceChange / previousPrice) * 100) : 0;
|
|
|
|
await db.insert(priceHistory).values({
|
|
monitorId,
|
|
price: price.toString(),
|
|
priceChange: priceChange.toString(),
|
|
percentChange: percentChange.toString(),
|
|
});
|
|
}
|
|
|
|
export async function getPriceHistory(monitorId: number, limit: number = 30) {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(priceHistory)
|
|
.where(eq(priceHistory.monitorId, monitorId))
|
|
.orderBy(desc(priceHistory.recordedAt))
|
|
.limit(limit);
|
|
}
|
|
|
|
export async function getActivePriceMonitors() {
|
|
const db = await getDb();
|
|
if (!db) return [];
|
|
return db.select().from(priceMonitors)
|
|
.where(eq(priceMonitors.isActive, true));
|
|
}
|