Merge branch 'toggleable-shelves'

This commit is contained in:
kikootwo
2026-03-11 10:02:57 -04:00
15 changed files with 167 additions and 40 deletions
@@ -13,7 +13,8 @@ import { z } from 'zod';
const logger = RMABLogger.create('API.GoodreadsShelves');
const UpdateGoodreadsSchema = z.object({
rssUrl: z.string().url('Must be a valid URL'),
rssUrl: z.string().url('Must be a valid URL').optional(),
autoRequest: z.boolean().optional(),
});
/**
@@ -81,21 +82,37 @@ export async function PATCH(
}
const body = await request.json();
const { rssUrl } = UpdateGoodreadsSchema.parse(body);
const { rssUrl, autoRequest } = UpdateGoodreadsSchema.parse(body);
const updateData: Record<string, unknown> = {};
let needsResync = false;
if (rssUrl !== undefined) {
updateData.rssUrl = rssUrl;
updateData.lastSyncAt = null;
updateData.bookCount = null;
updateData.coverUrls = null;
needsResync = true;
}
if (autoRequest !== undefined) {
updateData.autoRequest = autoRequest;
}
// Force re-fetch by clearing metadata
const updated = await prisma.goodreadsShelf.update({
where: { id },
data: { rssUrl, lastSyncAt: null, bookCount: null, coverUrls: null },
data: updateData,
});
try {
const jobQueue = getJobQueueService();
await jobQueue.addSyncShelvesJob(undefined, updated.id, 'goodreads', 0, req.user.id);
} catch (error) {
logger.error('Failed to trigger immediate list sync', {
error: error instanceof Error ? error.message : String(error),
});
if (needsResync) {
try {
const jobQueue = getJobQueueService();
await jobQueue.addSyncShelvesJob(undefined, updated.id, 'goodreads', 0, req.user.id);
} catch (error) {
logger.error('Failed to trigger immediate list sync', {
error: error instanceof Error ? error.message : String(error),
});
}
}
return NextResponse.json({ success: true, shelf: updated });
+5 -1
View File
@@ -20,6 +20,7 @@ const AddShelfSchema = z.object({
(url) => GOODREADS_RSS_PATTERN.test(url),
{ message: 'URL must be a Goodreads shelf RSS URL (goodreads.com/review/list_rss/...)' }
),
autoRequest: z.boolean().optional().default(true),
});
/**
@@ -66,6 +67,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt,
bookCount: shelf.bookCount ?? null,
autoRequest: shelf.autoRequest,
books,
};
});
@@ -90,7 +92,7 @@ export async function POST(request: NextRequest) {
}
const body = await req.json();
const { rssUrl } = AddShelfSchema.parse(body);
const { rssUrl, autoRequest } = AddShelfSchema.parse(body);
// Check for duplicate
const existing = await prisma.goodreadsShelf.findUnique({
@@ -132,6 +134,7 @@ export async function POST(request: NextRequest) {
name: shelfName,
rssUrl,
bookCount,
autoRequest,
coverUrls: initialBooks.length > 0 ? JSON.stringify(initialBooks) : null,
},
});
@@ -154,6 +157,7 @@ export async function POST(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt,
bookCount: shelf.bookCount,
autoRequest: shelf.autoRequest,
books: initialBooks,
},
bookCount,
@@ -18,6 +18,7 @@ const UpdateHardcoverSchema = z.object({
listId: z.string().min(1, 'List ID is required').optional(),
apiToken: z.string().optional(),
forceSync: z.boolean().optional(),
autoRequest: z.boolean().optional(),
});
/**
@@ -90,9 +91,13 @@ export async function PATCH(
}
const body = await request.json();
const { listId, apiToken, forceSync } = UpdateHardcoverSchema.parse(body);
const { listId, apiToken, forceSync, autoRequest } = UpdateHardcoverSchema.parse(body);
const updateData: { listId?: string; apiToken?: string; lastSyncAt?: null; bookCount?: null; coverUrls?: null } = {};
const updateData: { listId?: string; apiToken?: string; autoRequest?: boolean; lastSyncAt?: null; bookCount?: null; coverUrls?: null } = {};
if (autoRequest !== undefined) {
updateData.autoRequest = autoRequest;
}
let needsResync = !!forceSync;
let cleanedToken: string | undefined;
+7 -1
View File
@@ -18,6 +18,7 @@ const logger = RMABLogger.create('API.HardcoverShelves');
const AddShelfSchema = z.object({
listId: z.string().min(1, { message: 'List ID is required' }),
apiToken: z.string().min(1, { message: 'API Token is required' }),
autoRequest: z.boolean().optional().default(true),
});
/**
@@ -46,6 +47,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt,
bookCount: shelf.bookCount ?? null,
autoRequest: shelf.autoRequest,
books,
};
});
@@ -75,7 +77,9 @@ export async function POST(request: NextRequest) {
}
const body = await req.json();
let { listId, apiToken } = AddShelfSchema.parse(body);
const parsed = AddShelfSchema.parse(body);
let { listId, apiToken } = parsed;
const { autoRequest } = parsed;
// Clean up token in case user pasted "Bearer " prefix
apiToken = apiToken.trim();
@@ -139,6 +143,7 @@ export async function POST(request: NextRequest) {
name: listName,
listId,
apiToken: encryptedToken,
autoRequest,
bookCount,
coverUrls:
initialBooks.length > 0 ? JSON.stringify(initialBooks) : null,
@@ -168,6 +173,7 @@ export async function POST(request: NextRequest) {
lastSyncAt: shelf.lastSyncAt,
createdAt: shelf.createdAt,
bookCount: shelf.bookCount,
autoRequest: shelf.autoRequest,
books: initialBooks,
},
bookCount,
+2
View File
@@ -42,6 +42,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: s.lastSyncAt,
createdAt: s.createdAt,
bookCount: s.bookCount ?? null,
autoRequest: s.autoRequest,
books: processBooks(s.coverUrls),
})),
...hardcover.map((s) => ({
@@ -52,6 +53,7 @@ export async function GET(request: NextRequest) {
lastSyncAt: s.lastSyncAt,
createdAt: s.createdAt,
bookCount: s.bookCount ?? null,
autoRequest: s.autoRequest,
books: processBooks(s.coverUrls),
})),
].sort(