feat(18-03): add profile service and setup isPublic support
- updateProfile: update displayName, avatarUrl, bio for a user - getPublicProfile: return user info with only public setups - getPublicSetupWithItems: return setup details only if isPublic is true - createSetup now accepts and persists isPublic field - updateSetup can toggle isPublic - getAllSetups includes isPublic in response
This commit is contained in:
108
src/server/services/profile.service.ts
Normal file
108
src/server/services/profile.service.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
|
import { db as prodDb } from "../../db/index.ts";
|
||||||
|
import {
|
||||||
|
categories,
|
||||||
|
items,
|
||||||
|
setupItems,
|
||||||
|
setups,
|
||||||
|
users,
|
||||||
|
} from "../../db/schema.ts";
|
||||||
|
import type { UpdateProfile } from "../../shared/types.ts";
|
||||||
|
|
||||||
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
|
export async function updateProfile(
|
||||||
|
db: Db,
|
||||||
|
userId: number,
|
||||||
|
data: UpdateProfile,
|
||||||
|
) {
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId));
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
// If no fields to update, return existing user
|
||||||
|
const hasUpdates = Object.values(data).some((v) => v !== undefined);
|
||||||
|
if (!hasUpdates) return existing;
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(users)
|
||||||
|
.set(data)
|
||||||
|
.where(eq(users.id, userId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPublicProfile(db: Db, userId: number) {
|
||||||
|
const [user] = await db
|
||||||
|
.select({
|
||||||
|
id: users.id,
|
||||||
|
displayName: users.displayName,
|
||||||
|
avatarUrl: users.avatarUrl,
|
||||||
|
bio: users.bio,
|
||||||
|
})
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.id, userId));
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const publicSetups = await db
|
||||||
|
.select({
|
||||||
|
id: setups.id,
|
||||||
|
name: setups.name,
|
||||||
|
createdAt: setups.createdAt,
|
||||||
|
itemCount: sql<number>`COALESCE((
|
||||||
|
SELECT COUNT(*) FROM setup_items
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
), 0)`.as("item_count"),
|
||||||
|
totalWeight: sql<number>`COALESCE((
|
||||||
|
SELECT SUM(items.weight_grams * items.quantity) FROM setup_items
|
||||||
|
JOIN items ON items.id = setup_items.item_id
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
), 0)`.as("total_weight"),
|
||||||
|
totalCost: sql<number>`COALESCE((
|
||||||
|
SELECT SUM(items.price_cents * items.quantity) FROM setup_items
|
||||||
|
JOIN items ON items.id = setup_items.item_id
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
), 0)`.as("total_cost"),
|
||||||
|
})
|
||||||
|
.from(setups)
|
||||||
|
.where(and(eq(setups.userId, userId), eq(setups.isPublic, true)));
|
||||||
|
|
||||||
|
return { ...user, setups: publicSetups };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPublicSetupWithItems(db: Db, setupId: number) {
|
||||||
|
const [setup] = await db
|
||||||
|
.select()
|
||||||
|
.from(setups)
|
||||||
|
.where(and(eq(setups.id, setupId), eq(setups.isPublic, true)));
|
||||||
|
|
||||||
|
if (!setup) return null;
|
||||||
|
|
||||||
|
const itemList = await db
|
||||||
|
.select({
|
||||||
|
id: items.id,
|
||||||
|
name: items.name,
|
||||||
|
weightGrams: items.weightGrams,
|
||||||
|
priceCents: items.priceCents,
|
||||||
|
quantity: items.quantity,
|
||||||
|
categoryId: items.categoryId,
|
||||||
|
notes: items.notes,
|
||||||
|
productUrl: items.productUrl,
|
||||||
|
imageFilename: items.imageFilename,
|
||||||
|
createdAt: items.createdAt,
|
||||||
|
updatedAt: items.updatedAt,
|
||||||
|
categoryName: categories.name,
|
||||||
|
categoryIcon: categories.icon,
|
||||||
|
classification: setupItems.classification,
|
||||||
|
})
|
||||||
|
.from(setupItems)
|
||||||
|
.innerJoin(items, eq(setupItems.itemId, items.id))
|
||||||
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.where(eq(setupItems.setupId, setupId));
|
||||||
|
|
||||||
|
return { ...setup, items: itemList };
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export async function createSetup(
|
|||||||
) {
|
) {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name: data.name, userId })
|
.values({ name: data.name, userId, isPublic: data.isPublic ?? false })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
@@ -23,6 +23,7 @@ export async function getAllSetups(db: Db, userId: number) {
|
|||||||
.select({
|
.select({
|
||||||
id: setups.id,
|
id: setups.id,
|
||||||
name: setups.name,
|
name: setups.name,
|
||||||
|
isPublic: setups.isPublic,
|
||||||
createdAt: setups.createdAt,
|
createdAt: setups.createdAt,
|
||||||
updatedAt: setups.updatedAt,
|
updatedAt: setups.updatedAt,
|
||||||
itemCount: sql<number>`COALESCE((
|
itemCount: sql<number>`COALESCE((
|
||||||
@@ -92,9 +93,17 @@ export async function updateSetup(
|
|||||||
.where(and(eq(setups.id, setupId), eq(setups.userId, userId)));
|
.where(and(eq(setups.id, setupId), eq(setups.userId, userId)));
|
||||||
if (!existing) return null;
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
name: data.name,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
if (data.isPublic !== undefined) {
|
||||||
|
updateData.isPublic = data.isPublic;
|
||||||
|
}
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.update(setups)
|
.update(setups)
|
||||||
.set({ name: data.name, updatedAt: new Date() })
|
.set(updateData)
|
||||||
.where(and(eq(setups.id, setupId), eq(setups.userId, userId)))
|
.where(and(eq(setups.id, setupId), eq(setups.userId, userId)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user