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,
|
||||
"tag": "0004_smiling_night_nurse",
|
||||
"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 {
|
||||
id: number;
|
||||
name: string;
|
||||
isPublic?: boolean;
|
||||
visibility?: "private" | "link" | "public";
|
||||
itemCount: number;
|
||||
totalWeight: number;
|
||||
totalCost: number;
|
||||
@@ -13,7 +13,7 @@ interface SetupCardProps {
|
||||
export function SetupCard({
|
||||
id,
|
||||
name,
|
||||
isPublic,
|
||||
visibility,
|
||||
itemCount,
|
||||
totalWeight,
|
||||
totalCost,
|
||||
@@ -30,9 +30,15 @@ export function SetupCard({
|
||||
<h3 className="text-sm font-semibold text-gray-900 truncate">
|
||||
{name}
|
||||
</h3>
|
||||
{isPublic && (
|
||||
<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">
|
||||
Public
|
||||
{visibility && visibility !== "private" && (
|
||||
<span
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SetupsView() {
|
||||
key={setup.id}
|
||||
id={setup.id}
|
||||
name={setup.name}
|
||||
isPublic={setup.isPublic}
|
||||
visibility={setup.visibility}
|
||||
itemCount={setup.itemCount}
|
||||
totalWeight={setup.totalWeight}
|
||||
totalCost={setup.totalCost}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
interface SetupListItem {
|
||||
id: number;
|
||||
name: string;
|
||||
isPublic: boolean;
|
||||
visibility: "private" | "link" | "public";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
itemCount: number;
|
||||
@@ -39,7 +39,7 @@ interface SetupItemWithCategory {
|
||||
interface SetupWithItems {
|
||||
id: number;
|
||||
name: string;
|
||||
isPublic: boolean;
|
||||
visibility: "private" | "link" | "public";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
items: SetupItemWithCategory[];
|
||||
@@ -88,8 +88,10 @@ export function useCreateSetup() {
|
||||
export function useUpdateSetup(setupId: number) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: { name?: string; isPublic?: boolean }) =>
|
||||
apiPut<SetupListItem>(`/api/setups/${setupId}`, data),
|
||||
mutationFn: (data: {
|
||||
name?: string;
|
||||
visibility?: "private" | "link" | "public";
|
||||
}) => apiPut<SetupListItem>(`/api/setups/${setupId}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ function SetupDetailPage() {
|
||||
: publicSetup;
|
||||
|
||||
const deleteSetup = useDeleteSetup();
|
||||
const updateSetup = useUpdateSetup(numericId);
|
||||
const _updateSetup = useUpdateSetup(numericId);
|
||||
const removeItem = useRemoveSetupItem(numericId);
|
||||
const updateClassification = useUpdateItemClassification(numericId);
|
||||
|
||||
@@ -174,33 +174,60 @@ function SetupDetailPage() {
|
||||
<LucideIcon name="plus" size={16} />
|
||||
</button>
|
||||
|
||||
{/* Public toggle — desktop */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSetup.mutate({ isPublic: !setup.isPublic })}
|
||||
className={`hidden md:inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||
setup.isPublic
|
||||
? "text-green-700 bg-green-50 hover:bg-green-100"
|
||||
: "text-gray-500 bg-gray-50 hover:bg-gray-100"
|
||||
{/* Visibility badge — desktop */}
|
||||
<span
|
||||
className={`hidden md:inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium 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"
|
||||
}`}
|
||||
>
|
||||
<LucideIcon name="globe" size={16} />
|
||||
{setup.isPublic ? "Public" : "Private"}
|
||||
</button>
|
||||
{/* Public toggle — mobile */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateSetup.mutate({ isPublic: !setup.isPublic })}
|
||||
className={`md:hidden inline-flex items-center justify-center min-w-[44px] min-h-[44px] p-2 rounded-lg transition-colors ${
|
||||
setup.isPublic
|
||||
? "text-green-700 bg-green-50 hover:bg-green-100"
|
||||
: "text-gray-500 bg-gray-50 hover:bg-gray-100"
|
||||
<LucideIcon
|
||||
name={
|
||||
setup.visibility === "public"
|
||||
? "globe"
|
||||
: setup.visibility === "link"
|
||||
? "link"
|
||||
: "lock"
|
||||
}
|
||||
size={16}
|
||||
/>
|
||||
{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={setup.isPublic ? "Public" : "Private"}
|
||||
title={
|
||||
setup.visibility === "public"
|
||||
? "Public"
|
||||
: setup.visibility === "link"
|
||||
? "Link shared"
|
||||
: "Private"
|
||||
}
|
||||
>
|
||||
<LucideIcon name="globe" size={16} />
|
||||
</button>
|
||||
<LucideIcon
|
||||
name={
|
||||
setup.visibility === "public"
|
||||
? "globe"
|
||||
: setup.visibility === "link"
|
||||
? "link"
|
||||
: "lock"
|
||||
}
|
||||
size={16}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className="flex-1" />
|
||||
{/* Delete Setup — desktop */}
|
||||
|
||||
@@ -857,7 +857,7 @@ export const DEV_THREADS = [
|
||||
export const DEV_SETUPS = [
|
||||
{
|
||||
name: "Weekend Overnighter",
|
||||
isPublic: true,
|
||||
visibility: "public" as const,
|
||||
items: [
|
||||
{ userItemIndex: 0, classification: "base" }, // Terrapin saddle bag
|
||||
{ userItemIndex: 3, classification: "base" }, // X-Mid 1
|
||||
@@ -871,7 +871,7 @@ export const DEV_SETUPS = [
|
||||
},
|
||||
{
|
||||
name: "Ultra-Light Day Ride",
|
||||
isPublic: false,
|
||||
visibility: "private" as const,
|
||||
items: [
|
||||
{ userItemIndex: 2, classification: "base" }, // Top tube pack
|
||||
{ userItemIndex: 7, classification: "worn" }, // Nitecore NU25
|
||||
|
||||
@@ -252,7 +252,7 @@ async function seedDevData(database: Db = db) {
|
||||
.values({
|
||||
name: setupDef.name,
|
||||
userId,
|
||||
isPublic: setupDef.isPublic,
|
||||
visibility: setupDef.visibility,
|
||||
})
|
||||
.returning();
|
||||
if (!setup) throw new Error(`Failed to insert setup: ${setupDef.name}`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
boolean,
|
||||
doublePrecision,
|
||||
integer,
|
||||
pgTable,
|
||||
@@ -121,7 +120,7 @@ export const setups = pgTable("setups", {
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
isPublic: boolean("is_public").notNull().default(false),
|
||||
visibility: text("visibility").notNull().default("private"),
|
||||
createdAt: timestamp("created_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"),
|
||||
});
|
||||
|
||||
// ── 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 ────────────────────────────────────────────────────
|
||||
|
||||
export const globalItems = pgTable(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,12 +85,15 @@ export const reorderCandidatesSchema = z.object({
|
||||
// Setup schemas
|
||||
export const createSetupSchema = z.object({
|
||||
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({
|
||||
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({
|
||||
|
||||
@@ -20,6 +20,7 @@ async function getOrCreateDb(): Promise<Db> {
|
||||
|
||||
// Truncation order respects foreign keys (children first)
|
||||
const TRUNCATE_TABLES = [
|
||||
"shares",
|
||||
"setup_items",
|
||||
"setups",
|
||||
"thread_candidates",
|
||||
|
||||
@@ -40,7 +40,7 @@ async function insertPublicSetup(
|
||||
) {
|
||||
const [row] = await db
|
||||
.insert(setups)
|
||||
.values({ name, userId, isPublic: true })
|
||||
.values({ name, userId, visibility: "public" })
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ describe("Discovery Routes", () => {
|
||||
// Insert a private setup
|
||||
await db
|
||||
.insert(setups)
|
||||
.values({ name: "Private Setup", userId, isPublic: false });
|
||||
.values({ name: "Private Setup", userId, visibility: "private" });
|
||||
|
||||
const res = await app.request("/api/discovery/setups");
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
@@ -111,8 +111,8 @@ describe("Profile Routes", () => {
|
||||
it("includes only public setups", async () => {
|
||||
// Create public and private setups
|
||||
await db.insert(schema.setups).values([
|
||||
{ name: "Public Setup", userId, isPublic: true },
|
||||
{ name: "Private Setup", userId, isPublic: false },
|
||||
{ name: "Public Setup", userId, visibility: "public" },
|
||||
{ name: "Private Setup", userId, visibility: "private" },
|
||||
]);
|
||||
|
||||
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 () => {
|
||||
const [setup] = await db
|
||||
.insert(schema.setups)
|
||||
.values({ name: "My Public Setup", userId, isPublic: true })
|
||||
.values({ name: "My Public Setup", userId, visibility: "public" })
|
||||
.returning();
|
||||
|
||||
const res = await app.request(`/api/setups/${setup.id}/public`);
|
||||
@@ -189,14 +189,14 @@ describe("Public Setup Routes", () => {
|
||||
|
||||
const body = await res.json();
|
||||
expect(body.name).toBe("My Public Setup");
|
||||
expect(body.isPublic).toBe(true);
|
||||
expect(body.visibility).toBe("public");
|
||||
expect(body.items).toBeDefined();
|
||||
});
|
||||
|
||||
it("returns 200 for public setup with items", async () => {
|
||||
const [setup] = await db
|
||||
.insert(schema.setups)
|
||||
.values({ name: "Loaded Setup", userId, isPublic: true })
|
||||
.values({ name: "Loaded Setup", userId, visibility: "public" })
|
||||
.returning();
|
||||
|
||||
const [cat] = await db
|
||||
@@ -231,7 +231,7 @@ describe("Public Setup Routes", () => {
|
||||
it("returns 404 for private setup", async () => {
|
||||
const [setup] = await db
|
||||
.insert(schema.setups)
|
||||
.values({ name: "Private Setup", userId, isPublic: false })
|
||||
.values({ name: "Private Setup", userId, visibility: "private" })
|
||||
.returning();
|
||||
|
||||
const res = await app.request(`/api/setups/${setup.id}/public`);
|
||||
|
||||
@@ -47,7 +47,7 @@ async function insertPublicSetup(
|
||||
) {
|
||||
const [setup] = await db
|
||||
.insert(setups)
|
||||
.values({ name, userId, isPublic: true })
|
||||
.values({ name, userId, visibility: "public" })
|
||||
.returning();
|
||||
for (const itemId of itemIds) {
|
||||
await db.insert(setupItems).values({ setupId: setup.id, itemId });
|
||||
@@ -62,7 +62,7 @@ async function insertPrivateSetup(
|
||||
) {
|
||||
const [setup] = await db
|
||||
.insert(setups)
|
||||
.values({ name, userId, isPublic: false })
|
||||
.values({ name, userId, visibility: "private" })
|
||||
.returning();
|
||||
return setup;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("Profile Service", () => {
|
||||
// Create one public and one private setup
|
||||
const _pub = await createSetup(db, userId, {
|
||||
name: "Public Setup",
|
||||
isPublic: true,
|
||||
visibility: "public",
|
||||
});
|
||||
const _priv = await createSetup(db, userId, { name: "Private Setup" });
|
||||
|
||||
@@ -91,10 +91,10 @@ describe("Profile Service", () => {
|
||||
});
|
||||
|
||||
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, {
|
||||
name: "Public Setup",
|
||||
isPublic: true,
|
||||
visibility: "public",
|
||||
});
|
||||
|
||||
// Create an item and add to setup
|
||||
@@ -125,7 +125,7 @@ describe("Profile Service", () => {
|
||||
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, {
|
||||
name: "Private Setup",
|
||||
});
|
||||
@@ -140,7 +140,7 @@ describe("Profile Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Setup Service - isPublic", () => {
|
||||
describe("Setup Service - visibility", () => {
|
||||
let db: Db;
|
||||
let userId: number;
|
||||
|
||||
@@ -150,33 +150,33 @@ describe("Setup Service - isPublic", () => {
|
||||
userId = testData.userId;
|
||||
});
|
||||
|
||||
it("createSetup persists isPublic when true", async () => {
|
||||
it("createSetup persists visibility when public", async () => {
|
||||
const setup = await createSetup(db, userId, {
|
||||
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" });
|
||||
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" });
|
||||
expect(setup.isPublic).toBe(false);
|
||||
expect(setup.visibility).toBe("private");
|
||||
|
||||
const updated = await updateSetup(db, userId, setup.id, {
|
||||
name: "Test",
|
||||
isPublic: true,
|
||||
visibility: "public",
|
||||
});
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.isPublic).toBe(true);
|
||||
expect(updated!.visibility).toBe("public");
|
||||
});
|
||||
|
||||
it("getAllSetups includes isPublic in response", async () => {
|
||||
await createSetup(db, userId, { name: "Public", isPublic: true });
|
||||
it("getAllSetups includes visibility in response", async () => {
|
||||
await createSetup(db, userId, { name: "Public", visibility: "public" });
|
||||
await createSetup(db, userId, { name: "Private" });
|
||||
|
||||
const setups = await getAllSetups(db, userId);
|
||||
@@ -184,17 +184,17 @@ describe("Setup Service - isPublic", () => {
|
||||
|
||||
const pub = setups.find((s) => s.name === "Public");
|
||||
const priv = setups.find((s) => s.name === "Private");
|
||||
expect(pub!.isPublic).toBe(true);
|
||||
expect(priv!.isPublic).toBe(false);
|
||||
expect(pub!.visibility).toBe("public");
|
||||
expect(priv!.visibility).toBe("private");
|
||||
});
|
||||
|
||||
it("getSetupWithItems includes isPublic", async () => {
|
||||
it("getSetupWithItems includes visibility", async () => {
|
||||
const setup = await createSetup(db, userId, {
|
||||
name: "Test",
|
||||
isPublic: true,
|
||||
visibility: "public",
|
||||
});
|
||||
const result = await getSetupWithItems(db, userId, setup.id);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.isPublic).toBe(true);
|
||||
expect(result!.visibility).toBe("public");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user