feat: migrate setup visibility from boolean to three-tier system

Replace isPublic boolean with visibility enum (private/link/public) across
the full stack. Add shares table to schema for future share link support.
Update all services, routes, schemas, hooks, components, and tests.

Plan: 32-01 (Setup Sharing System - Schema Migration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 17:55:46 +02:00
parent 727abf1528
commit edc9793c2d
20 changed files with 1556 additions and 81 deletions

View File

@@ -114,7 +114,7 @@ app.post("/delete", zValidator("json", deleteAccountSchema), async (c) => {
await tx
.update(setups)
.set({ userId: sentinel.id })
.where(and(eq(setups.userId, userId), eq(setups.isPublic, true)));
.where(and(eq(setups.userId, userId), eq(setups.visibility, "public")));
// 3. Get private setup IDs for cleanup
const privateSetups = await tx

View File

@@ -21,7 +21,7 @@ interface CursorPage<T> {
/**
* Get popular public setups ordered by item count descending.
* Cursor format: "{itemCount}_{id}" for stable composite pagination.
* Only public setups (isPublic=true) are returned.
* Only public setups (visibility='public') are returned.
*/
export async function getPopularSetups(
db: Db = prodDb,
@@ -50,7 +50,7 @@ export async function getPopularSetups(
.from(setups)
.leftJoin(setupItems, eq(setupItems.setupId, setups.id))
.leftJoin(users, eq(users.id, setups.userId))
.where(eq(setups.isPublic, true))
.where(eq(setups.visibility, "public"))
.groupBy(setups.id, setups.name, setups.createdAt, users.displayName)
.orderBy(desc(sql<number>`COUNT(${setupItems.id})`), desc(setups.id))
.limit(fetchLimit);

View File

@@ -79,7 +79,7 @@ export async function getPublicProfile(db: Db, userId: number) {
), 0)`.as("total_cost"),
})
.from(setups)
.where(and(eq(setups.userId, userId), eq(setups.isPublic, true)));
.where(and(eq(setups.userId, userId), eq(setups.visibility, "public")));
return { ...user, setups: publicSetups };
}
@@ -88,7 +88,7 @@ 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)));
.where(and(eq(setups.id, setupId), eq(setups.visibility, "public")));
if (!setup) return null;

View File

@@ -14,7 +14,11 @@ type Db = typeof prodDb;
export async function createSetup(db: Db, userId: number, data: CreateSetup) {
const [row] = await db
.insert(setups)
.values({ name: data.name, userId, isPublic: data.isPublic ?? false })
.values({
name: data.name,
userId,
visibility: data.visibility ?? "private",
})
.returning();
return row;
@@ -25,7 +29,7 @@ export async function getAllSetups(db: Db, userId: number) {
.select({
id: setups.id,
name: setups.name,
isPublic: setups.isPublic,
visibility: setups.visibility,
createdAt: setups.createdAt,
updatedAt: setups.updatedAt,
itemCount: sql<number>`COALESCE((
@@ -129,8 +133,8 @@ export async function updateSetup(
name: data.name,
updatedAt: new Date(),
};
if (data.isPublic !== undefined) {
updateData.isPublic = data.isPublic;
if (data.visibility !== undefined) {
updateData.visibility = data.visibility;
}
const [row] = await db