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:
17
drizzle-pg/0005_true_green_goblin.sql
Normal file
17
drizzle-pg/0005_true_green_goblin.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE "shares" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"setup_id" integer NOT NULL,
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"permission" text DEFAULT 'read' NOT NULL,
|
||||||
|
"expires_at" timestamp,
|
||||||
|
"user_id" integer,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"revoked_at" timestamp,
|
||||||
|
CONSTRAINT "shares_token_unique" UNIQUE("token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "setups" ADD COLUMN "visibility" text DEFAULT 'private' NOT NULL;--> statement-breakpoint
|
||||||
|
UPDATE "setups" SET "visibility" = 'public' WHERE "is_public" = true;--> statement-breakpoint
|
||||||
|
ALTER TABLE "shares" ADD CONSTRAINT "shares_setup_id_setups_id_fk" FOREIGN KEY ("setup_id") REFERENCES "public"."setups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "shares" ADD CONSTRAINT "shares_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "setups" DROP COLUMN "is_public";
|
||||||
1394
drizzle-pg/meta/0005_snapshot.json
Normal file
1394
drizzle-pg/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
|||||||
"when": 1776016552627,
|
"when": 1776016552627,
|
||||||
"tag": "0004_smiling_night_nurse",
|
"tag": "0004_smiling_night_nurse",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776095449827,
|
||||||
|
"tag": "0005_true_green_goblin",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import { useFormatters } from "../hooks/useFormatters";
|
|||||||
interface SetupCardProps {
|
interface SetupCardProps {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
isPublic?: boolean;
|
visibility?: "private" | "link" | "public";
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
totalWeight: number;
|
totalWeight: number;
|
||||||
totalCost: number;
|
totalCost: number;
|
||||||
@@ -13,7 +13,7 @@ interface SetupCardProps {
|
|||||||
export function SetupCard({
|
export function SetupCard({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
isPublic,
|
visibility,
|
||||||
itemCount,
|
itemCount,
|
||||||
totalWeight,
|
totalWeight,
|
||||||
totalCost,
|
totalCost,
|
||||||
@@ -30,9 +30,15 @@ export function SetupCard({
|
|||||||
<h3 className="text-sm font-semibold text-gray-900 truncate">
|
<h3 className="text-sm font-semibold text-gray-900 truncate">
|
||||||
{name}
|
{name}
|
||||||
</h3>
|
</h3>
|
||||||
{isPublic && (
|
{visibility && visibility !== "private" && (
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-green-50 text-green-600 shrink-0">
|
<span
|
||||||
Public
|
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-medium shrink-0 ${
|
||||||
|
visibility === "public"
|
||||||
|
? "bg-green-50 text-green-600"
|
||||||
|
: "bg-blue-50 text-blue-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{visibility === "public" ? "Public" : "Link"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function SetupsView() {
|
|||||||
key={setup.id}
|
key={setup.id}
|
||||||
id={setup.id}
|
id={setup.id}
|
||||||
name={setup.name}
|
name={setup.name}
|
||||||
isPublic={setup.isPublic}
|
visibility={setup.visibility}
|
||||||
itemCount={setup.itemCount}
|
itemCount={setup.itemCount}
|
||||||
totalWeight={setup.totalWeight}
|
totalWeight={setup.totalWeight}
|
||||||
totalCost={setup.totalCost}
|
totalCost={setup.totalCost}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
interface SetupListItem {
|
interface SetupListItem {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
isPublic: boolean;
|
visibility: "private" | "link" | "public";
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
@@ -39,7 +39,7 @@ interface SetupItemWithCategory {
|
|||||||
interface SetupWithItems {
|
interface SetupWithItems {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
isPublic: boolean;
|
visibility: "private" | "link" | "public";
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
items: SetupItemWithCategory[];
|
items: SetupItemWithCategory[];
|
||||||
@@ -88,8 +88,10 @@ export function useCreateSetup() {
|
|||||||
export function useUpdateSetup(setupId: number) {
|
export function useUpdateSetup(setupId: number) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (data: { name?: string; isPublic?: boolean }) =>
|
mutationFn: (data: {
|
||||||
apiPut<SetupListItem>(`/api/setups/${setupId}`, data),
|
name?: string;
|
||||||
|
visibility?: "private" | "link" | "public";
|
||||||
|
}) => apiPut<SetupListItem>(`/api/setups/${setupId}`, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function SetupDetailPage() {
|
|||||||
: publicSetup;
|
: publicSetup;
|
||||||
|
|
||||||
const deleteSetup = useDeleteSetup();
|
const deleteSetup = useDeleteSetup();
|
||||||
const updateSetup = useUpdateSetup(numericId);
|
const _updateSetup = useUpdateSetup(numericId);
|
||||||
const removeItem = useRemoveSetupItem(numericId);
|
const removeItem = useRemoveSetupItem(numericId);
|
||||||
const updateClassification = useUpdateItemClassification(numericId);
|
const updateClassification = useUpdateItemClassification(numericId);
|
||||||
|
|
||||||
@@ -174,33 +174,60 @@ function SetupDetailPage() {
|
|||||||
<LucideIcon name="plus" size={16} />
|
<LucideIcon name="plus" size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Public toggle — desktop */}
|
{/* Visibility badge — desktop */}
|
||||||
<button
|
<span
|
||||||
type="button"
|
className={`hidden md:inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg ${
|
||||||
onClick={() => updateSetup.mutate({ isPublic: !setup.isPublic })}
|
setup.visibility === "public"
|
||||||
className={`hidden md:inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
? "text-green-700 bg-green-50"
|
||||||
setup.isPublic
|
: setup.visibility === "link"
|
||||||
? "text-green-700 bg-green-50 hover:bg-green-100"
|
? "text-blue-600 bg-blue-50"
|
||||||
: "text-gray-500 bg-gray-50 hover:bg-gray-100"
|
: "text-gray-500 bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<LucideIcon name="globe" size={16} />
|
<LucideIcon
|
||||||
{setup.isPublic ? "Public" : "Private"}
|
name={
|
||||||
</button>
|
setup.visibility === "public"
|
||||||
{/* Public toggle — mobile */}
|
? "globe"
|
||||||
<button
|
: setup.visibility === "link"
|
||||||
type="button"
|
? "link"
|
||||||
onClick={() => updateSetup.mutate({ isPublic: !setup.isPublic })}
|
: "lock"
|
||||||
className={`md:hidden inline-flex items-center justify-center min-w-[44px] min-h-[44px] p-2 rounded-lg transition-colors ${
|
}
|
||||||
setup.isPublic
|
size={16}
|
||||||
? "text-green-700 bg-green-50 hover:bg-green-100"
|
/>
|
||||||
: "text-gray-500 bg-gray-50 hover:bg-gray-100"
|
{setup.visibility === "public"
|
||||||
|
? "Public"
|
||||||
|
: setup.visibility === "link"
|
||||||
|
? "Link"
|
||||||
|
: "Private"}
|
||||||
|
</span>
|
||||||
|
{/* Visibility badge — mobile */}
|
||||||
|
<span
|
||||||
|
className={`md:hidden inline-flex items-center justify-center min-w-[44px] min-h-[44px] p-2 rounded-lg ${
|
||||||
|
setup.visibility === "public"
|
||||||
|
? "text-green-700 bg-green-50"
|
||||||
|
: setup.visibility === "link"
|
||||||
|
? "text-blue-600 bg-blue-50"
|
||||||
|
: "text-gray-500 bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
aria-label={setup.isPublic ? "Public" : "Private"}
|
title={
|
||||||
title={setup.isPublic ? "Public" : "Private"}
|
setup.visibility === "public"
|
||||||
|
? "Public"
|
||||||
|
: setup.visibility === "link"
|
||||||
|
? "Link shared"
|
||||||
|
: "Private"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<LucideIcon name="globe" size={16} />
|
<LucideIcon
|
||||||
</button>
|
name={
|
||||||
|
setup.visibility === "public"
|
||||||
|
? "globe"
|
||||||
|
: setup.visibility === "link"
|
||||||
|
? "link"
|
||||||
|
: "lock"
|
||||||
|
}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{/* Delete Setup — desktop */}
|
{/* Delete Setup — desktop */}
|
||||||
|
|||||||
@@ -857,7 +857,7 @@ export const DEV_THREADS = [
|
|||||||
export const DEV_SETUPS = [
|
export const DEV_SETUPS = [
|
||||||
{
|
{
|
||||||
name: "Weekend Overnighter",
|
name: "Weekend Overnighter",
|
||||||
isPublic: true,
|
visibility: "public" as const,
|
||||||
items: [
|
items: [
|
||||||
{ userItemIndex: 0, classification: "base" }, // Terrapin saddle bag
|
{ userItemIndex: 0, classification: "base" }, // Terrapin saddle bag
|
||||||
{ userItemIndex: 3, classification: "base" }, // X-Mid 1
|
{ userItemIndex: 3, classification: "base" }, // X-Mid 1
|
||||||
@@ -871,7 +871,7 @@ export const DEV_SETUPS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ultra-Light Day Ride",
|
name: "Ultra-Light Day Ride",
|
||||||
isPublic: false,
|
visibility: "private" as const,
|
||||||
items: [
|
items: [
|
||||||
{ userItemIndex: 2, classification: "base" }, // Top tube pack
|
{ userItemIndex: 2, classification: "base" }, // Top tube pack
|
||||||
{ userItemIndex: 7, classification: "worn" }, // Nitecore NU25
|
{ userItemIndex: 7, classification: "worn" }, // Nitecore NU25
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ async function seedDevData(database: Db = db) {
|
|||||||
.values({
|
.values({
|
||||||
name: setupDef.name,
|
name: setupDef.name,
|
||||||
userId,
|
userId,
|
||||||
isPublic: setupDef.isPublic,
|
visibility: setupDef.visibility,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
if (!setup) throw new Error(`Failed to insert setup: ${setupDef.name}`);
|
if (!setup) throw new Error(`Failed to insert setup: ${setupDef.name}`);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
boolean,
|
|
||||||
doublePrecision,
|
doublePrecision,
|
||||||
integer,
|
integer,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -121,7 +120,7 @@ export const setups = pgTable("setups", {
|
|||||||
userId: integer("user_id")
|
userId: integer("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id),
|
.references(() => users.id),
|
||||||
isPublic: boolean("is_public").notNull().default(false),
|
visibility: text("visibility").notNull().default("private"),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
@@ -139,6 +138,21 @@ export const setupItems = pgTable("setup_items", {
|
|||||||
classification: text("classification").notNull().default("base"),
|
classification: text("classification").notNull().default("base"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Shares ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const shares = pgTable("shares", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
setupId: integer("setup_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => setups.id, { onDelete: "cascade" }),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
permission: text("permission").notNull().default("read"),
|
||||||
|
expiresAt: timestamp("expires_at"),
|
||||||
|
userId: integer("user_id").references(() => users.id),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
revokedAt: timestamp("revoked_at"),
|
||||||
|
});
|
||||||
|
|
||||||
// ── Global Items ────────────────────────────────────────────────────
|
// ── Global Items ────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const globalItems = pgTable(
|
export const globalItems = pgTable(
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ app.post("/delete", zValidator("json", deleteAccountSchema), async (c) => {
|
|||||||
await tx
|
await tx
|
||||||
.update(setups)
|
.update(setups)
|
||||||
.set({ userId: sentinel.id })
|
.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
|
// 3. Get private setup IDs for cleanup
|
||||||
const privateSetups = await tx
|
const privateSetups = await tx
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface CursorPage<T> {
|
|||||||
/**
|
/**
|
||||||
* Get popular public setups ordered by item count descending.
|
* Get popular public setups ordered by item count descending.
|
||||||
* Cursor format: "{itemCount}_{id}" for stable composite pagination.
|
* 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(
|
export async function getPopularSetups(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
@@ -50,7 +50,7 @@ export async function getPopularSetups(
|
|||||||
.from(setups)
|
.from(setups)
|
||||||
.leftJoin(setupItems, eq(setupItems.setupId, setups.id))
|
.leftJoin(setupItems, eq(setupItems.setupId, setups.id))
|
||||||
.leftJoin(users, eq(users.id, setups.userId))
|
.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)
|
.groupBy(setups.id, setups.name, setups.createdAt, users.displayName)
|
||||||
.orderBy(desc(sql<number>`COUNT(${setupItems.id})`), desc(setups.id))
|
.orderBy(desc(sql<number>`COUNT(${setupItems.id})`), desc(setups.id))
|
||||||
.limit(fetchLimit);
|
.limit(fetchLimit);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export async function getPublicProfile(db: Db, userId: number) {
|
|||||||
), 0)`.as("total_cost"),
|
), 0)`.as("total_cost"),
|
||||||
})
|
})
|
||||||
.from(setups)
|
.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 };
|
return { ...user, setups: publicSetups };
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export async function getPublicSetupWithItems(db: Db, setupId: number) {
|
|||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(setups)
|
.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;
|
if (!setup) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ type Db = typeof prodDb;
|
|||||||
export async function createSetup(db: Db, userId: number, data: CreateSetup) {
|
export async function createSetup(db: Db, userId: number, data: CreateSetup) {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name: data.name, userId, isPublic: data.isPublic ?? false })
|
.values({
|
||||||
|
name: data.name,
|
||||||
|
userId,
|
||||||
|
visibility: data.visibility ?? "private",
|
||||||
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
@@ -25,7 +29,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,
|
visibility: setups.visibility,
|
||||||
createdAt: setups.createdAt,
|
createdAt: setups.createdAt,
|
||||||
updatedAt: setups.updatedAt,
|
updatedAt: setups.updatedAt,
|
||||||
itemCount: sql<number>`COALESCE((
|
itemCount: sql<number>`COALESCE((
|
||||||
@@ -129,8 +133,8 @@ export async function updateSetup(
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
if (data.isPublic !== undefined) {
|
if (data.visibility !== undefined) {
|
||||||
updateData.isPublic = data.isPublic;
|
updateData.visibility = data.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
|
|||||||
@@ -85,12 +85,15 @@ export const reorderCandidatesSchema = z.object({
|
|||||||
// Setup schemas
|
// Setup schemas
|
||||||
export const createSetupSchema = z.object({
|
export const createSetupSchema = z.object({
|
||||||
name: z.string().min(1, "Setup name is required"),
|
name: z.string().min(1, "Setup name is required"),
|
||||||
isPublic: z.boolean().optional().default(false),
|
visibility: z
|
||||||
|
.enum(["private", "link", "public"])
|
||||||
|
.optional()
|
||||||
|
.default("private"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateSetupSchema = z.object({
|
export const updateSetupSchema = z.object({
|
||||||
name: z.string().min(1, "Setup name is required"),
|
name: z.string().min(1, "Setup name is required"),
|
||||||
isPublic: z.boolean().optional(),
|
visibility: z.enum(["private", "link", "public"]).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const syncSetupItemsSchema = z.object({
|
export const syncSetupItemsSchema = z.object({
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ async function getOrCreateDb(): Promise<Db> {
|
|||||||
|
|
||||||
// Truncation order respects foreign keys (children first)
|
// Truncation order respects foreign keys (children first)
|
||||||
const TRUNCATE_TABLES = [
|
const TRUNCATE_TABLES = [
|
||||||
|
"shares",
|
||||||
"setup_items",
|
"setup_items",
|
||||||
"setups",
|
"setups",
|
||||||
"thread_candidates",
|
"thread_candidates",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async function insertPublicSetup(
|
|||||||
) {
|
) {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name, userId, isPublic: true })
|
.values({ name, userId, visibility: "public" })
|
||||||
.returning();
|
.returning();
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ describe("Discovery Routes", () => {
|
|||||||
// Insert a private setup
|
// Insert a private setup
|
||||||
await db
|
await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name: "Private Setup", userId, isPublic: false });
|
.values({ name: "Private Setup", userId, visibility: "private" });
|
||||||
|
|
||||||
const res = await app.request("/api/discovery/setups");
|
const res = await app.request("/api/discovery/setups");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ describe("Profile Routes", () => {
|
|||||||
it("includes only public setups", async () => {
|
it("includes only public setups", async () => {
|
||||||
// Create public and private setups
|
// Create public and private setups
|
||||||
await db.insert(schema.setups).values([
|
await db.insert(schema.setups).values([
|
||||||
{ name: "Public Setup", userId, isPublic: true },
|
{ name: "Public Setup", userId, visibility: "public" },
|
||||||
{ name: "Private Setup", userId, isPublic: false },
|
{ name: "Private Setup", userId, visibility: "private" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const res = await app.request(`/api/users/${userId}/profile`);
|
const res = await app.request(`/api/users/${userId}/profile`);
|
||||||
@@ -181,7 +181,7 @@ describe("Public Setup Routes", () => {
|
|||||||
it("returns 200 for public setup without auth", async () => {
|
it("returns 200 for public setup without auth", async () => {
|
||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.insert(schema.setups)
|
.insert(schema.setups)
|
||||||
.values({ name: "My Public Setup", userId, isPublic: true })
|
.values({ name: "My Public Setup", userId, visibility: "public" })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const res = await app.request(`/api/setups/${setup.id}/public`);
|
const res = await app.request(`/api/setups/${setup.id}/public`);
|
||||||
@@ -189,14 +189,14 @@ describe("Public Setup Routes", () => {
|
|||||||
|
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.name).toBe("My Public Setup");
|
expect(body.name).toBe("My Public Setup");
|
||||||
expect(body.isPublic).toBe(true);
|
expect(body.visibility).toBe("public");
|
||||||
expect(body.items).toBeDefined();
|
expect(body.items).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 200 for public setup with items", async () => {
|
it("returns 200 for public setup with items", async () => {
|
||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.insert(schema.setups)
|
.insert(schema.setups)
|
||||||
.values({ name: "Loaded Setup", userId, isPublic: true })
|
.values({ name: "Loaded Setup", userId, visibility: "public" })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const [cat] = await db
|
const [cat] = await db
|
||||||
@@ -231,7 +231,7 @@ describe("Public Setup Routes", () => {
|
|||||||
it("returns 404 for private setup", async () => {
|
it("returns 404 for private setup", async () => {
|
||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.insert(schema.setups)
|
.insert(schema.setups)
|
||||||
.values({ name: "Private Setup", userId, isPublic: false })
|
.values({ name: "Private Setup", userId, visibility: "private" })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const res = await app.request(`/api/setups/${setup.id}/public`);
|
const res = await app.request(`/api/setups/${setup.id}/public`);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function insertPublicSetup(
|
|||||||
) {
|
) {
|
||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name, userId, isPublic: true })
|
.values({ name, userId, visibility: "public" })
|
||||||
.returning();
|
.returning();
|
||||||
for (const itemId of itemIds) {
|
for (const itemId of itemIds) {
|
||||||
await db.insert(setupItems).values({ setupId: setup.id, itemId });
|
await db.insert(setupItems).values({ setupId: setup.id, itemId });
|
||||||
@@ -62,7 +62,7 @@ async function insertPrivateSetup(
|
|||||||
) {
|
) {
|
||||||
const [setup] = await db
|
const [setup] = await db
|
||||||
.insert(setups)
|
.insert(setups)
|
||||||
.values({ name, userId, isPublic: false })
|
.values({ name, userId, visibility: "private" })
|
||||||
.returning();
|
.returning();
|
||||||
return setup;
|
return setup;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe("Profile Service", () => {
|
|||||||
// Create one public and one private setup
|
// Create one public and one private setup
|
||||||
const _pub = await createSetup(db, userId, {
|
const _pub = await createSetup(db, userId, {
|
||||||
name: "Public Setup",
|
name: "Public Setup",
|
||||||
isPublic: true,
|
visibility: "public",
|
||||||
});
|
});
|
||||||
const _priv = await createSetup(db, userId, { name: "Private Setup" });
|
const _priv = await createSetup(db, userId, { name: "Private Setup" });
|
||||||
|
|
||||||
@@ -91,10 +91,10 @@ describe("Profile Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getPublicSetupWithItems", () => {
|
describe("getPublicSetupWithItems", () => {
|
||||||
it("returns setup with items when isPublic is true", async () => {
|
it("returns setup with items when visibility is public", async () => {
|
||||||
const setup = await createSetup(db, userId, {
|
const setup = await createSetup(db, userId, {
|
||||||
name: "Public Setup",
|
name: "Public Setup",
|
||||||
isPublic: true,
|
visibility: "public",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create an item and add to setup
|
// Create an item and add to setup
|
||||||
@@ -125,7 +125,7 @@ describe("Profile Service", () => {
|
|||||||
expect(result!.items[0].name).toBe("Tent");
|
expect(result!.items[0].name).toBe("Tent");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when isPublic is false", async () => {
|
it("returns null when visibility is private", async () => {
|
||||||
const setup = await createSetup(db, userId, {
|
const setup = await createSetup(db, userId, {
|
||||||
name: "Private Setup",
|
name: "Private Setup",
|
||||||
});
|
});
|
||||||
@@ -140,7 +140,7 @@ describe("Profile Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Setup Service - isPublic", () => {
|
describe("Setup Service - visibility", () => {
|
||||||
let db: Db;
|
let db: Db;
|
||||||
let userId: number;
|
let userId: number;
|
||||||
|
|
||||||
@@ -150,33 +150,33 @@ describe("Setup Service - isPublic", () => {
|
|||||||
userId = testData.userId;
|
userId = testData.userId;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("createSetup persists isPublic when true", async () => {
|
it("createSetup persists visibility when public", async () => {
|
||||||
const setup = await createSetup(db, userId, {
|
const setup = await createSetup(db, userId, {
|
||||||
name: "Public",
|
name: "Public",
|
||||||
isPublic: true,
|
visibility: "public",
|
||||||
});
|
});
|
||||||
expect(setup.isPublic).toBe(true);
|
expect(setup.visibility).toBe("public");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("createSetup defaults isPublic to false", async () => {
|
it("createSetup defaults visibility to private", async () => {
|
||||||
const setup = await createSetup(db, userId, { name: "Private" });
|
const setup = await createSetup(db, userId, { name: "Private" });
|
||||||
expect(setup.isPublic).toBe(false);
|
expect(setup.visibility).toBe("private");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updateSetup can toggle isPublic", async () => {
|
it("updateSetup can change visibility", async () => {
|
||||||
const setup = await createSetup(db, userId, { name: "Test" });
|
const setup = await createSetup(db, userId, { name: "Test" });
|
||||||
expect(setup.isPublic).toBe(false);
|
expect(setup.visibility).toBe("private");
|
||||||
|
|
||||||
const updated = await updateSetup(db, userId, setup.id, {
|
const updated = await updateSetup(db, userId, setup.id, {
|
||||||
name: "Test",
|
name: "Test",
|
||||||
isPublic: true,
|
visibility: "public",
|
||||||
});
|
});
|
||||||
expect(updated).not.toBeNull();
|
expect(updated).not.toBeNull();
|
||||||
expect(updated!.isPublic).toBe(true);
|
expect(updated!.visibility).toBe("public");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getAllSetups includes isPublic in response", async () => {
|
it("getAllSetups includes visibility in response", async () => {
|
||||||
await createSetup(db, userId, { name: "Public", isPublic: true });
|
await createSetup(db, userId, { name: "Public", visibility: "public" });
|
||||||
await createSetup(db, userId, { name: "Private" });
|
await createSetup(db, userId, { name: "Private" });
|
||||||
|
|
||||||
const setups = await getAllSetups(db, userId);
|
const setups = await getAllSetups(db, userId);
|
||||||
@@ -184,17 +184,17 @@ describe("Setup Service - isPublic", () => {
|
|||||||
|
|
||||||
const pub = setups.find((s) => s.name === "Public");
|
const pub = setups.find((s) => s.name === "Public");
|
||||||
const priv = setups.find((s) => s.name === "Private");
|
const priv = setups.find((s) => s.name === "Private");
|
||||||
expect(pub!.isPublic).toBe(true);
|
expect(pub!.visibility).toBe("public");
|
||||||
expect(priv!.isPublic).toBe(false);
|
expect(priv!.visibility).toBe("private");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getSetupWithItems includes isPublic", async () => {
|
it("getSetupWithItems includes visibility", async () => {
|
||||||
const setup = await createSetup(db, userId, {
|
const setup = await createSetup(db, userId, {
|
||||||
name: "Test",
|
name: "Test",
|
||||||
isPublic: true,
|
visibility: "public",
|
||||||
});
|
});
|
||||||
const result = await getSetupWithItems(db, userId, setup.id);
|
const result = await getSetupWithItems(db, userId, setup.id);
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!.isPublic).toBe(true);
|
expect(result!.visibility).toBe("public");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user