feat(ItemsList, database): enhance item update logic with validation and improved error handling
This commit is contained in:
parent
887815c7da
commit
dfad78a0ca
3 changed files with 196 additions and 106 deletions
|
@ -1,6 +1,62 @@
|
|||
//Handles DELETE requests for specific items
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { deleteItemByUrl } from '@/lib/database';
|
||||
import { deleteItemByUrl, insertItemByUrl } from '@/lib/database';
|
||||
import { Item } from '@/types/database';
|
||||
|
||||
// PUT /api/urls/[encodedUrl]/items/[itemId] - Update a specific item
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { encodedUrl: string; itemId: string } }
|
||||
) {
|
||||
try {
|
||||
const { encodedUrl, itemId } = params;
|
||||
const url = decodeURIComponent(encodedUrl);
|
||||
const item: Item = await request.json();
|
||||
|
||||
console.log('[API] Updating item', itemId, 'for URL', url);
|
||||
|
||||
// Validate that the item ID matches the URL parameter
|
||||
if (item.id !== itemId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Item ID mismatch' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!item.id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Item ID is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure name and description are strings
|
||||
const sanitizedItem: Item = {
|
||||
...item,
|
||||
name: item.name || '',
|
||||
description: item.description || '',
|
||||
customProperties: item.customProperties || {}
|
||||
};
|
||||
|
||||
await insertItemByUrl(url, sanitizedItem);
|
||||
|
||||
console.log('[API] Successfully updated item:', itemId);
|
||||
|
||||
return NextResponse.json({ success: true, itemId });
|
||||
} catch (error) {
|
||||
console.error('[API] Update error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Database error',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
itemId: params.itemId,
|
||||
url: decodeURIComponent(params.encodedUrl)
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/urls/[encodedUrl]/items/[itemId] - Delete a specific item
|
||||
export async function DELETE(
|
||||
|
|
|
@ -344,34 +344,53 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
|
||||
newItems[index] = item;
|
||||
|
||||
// Auto-save with error handling
|
||||
saveItemMutation.mutate(item, {
|
||||
onError: (error) => {
|
||||
console.error('Failed to save item:', error);
|
||||
// You could add a toast notification here
|
||||
}
|
||||
});
|
||||
// Only save if the item has some content
|
||||
const hasContent = item.name.trim() ||
|
||||
item.description.trim() ||
|
||||
Object.values(item.customProperties).some(prop => prop.trim());
|
||||
|
||||
// Add new row if editing last row and value is not empty
|
||||
if (index === newItems.length - 1 && value.trim()) {
|
||||
const newItem: Item = {
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
description: '',
|
||||
wikidataId: undefined,
|
||||
customProperties: {}
|
||||
};
|
||||
newItems.push(newItem);
|
||||
saveItemMutation.mutate(newItem, {
|
||||
if (hasContent) {
|
||||
// Auto-save with error handling
|
||||
saveItemMutation.mutate(item, {
|
||||
onError: (error) => {
|
||||
console.error('Failed to save new item:', error);
|
||||
console.error('Failed to save item:', error);
|
||||
// You could add a toast notification here
|
||||
},
|
||||
onSuccess: () => {
|
||||
// After successful save, reload items to get any synchronized properties
|
||||
// This ensures that if another item with the same name exists,
|
||||
// its properties are updated in the UI
|
||||
queryClient.invalidateQueries({ queryKey: ['items', url] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add new row if editing last row and value is not empty
|
||||
// Only add if the last item doesn't already have content and we're not already at the end
|
||||
if (index === newItems.length - 1 && value.trim()) {
|
||||
const lastItem = newItems[newItems.length - 1];
|
||||
const lastItemHasContent = lastItem.name.trim() ||
|
||||
lastItem.description.trim() ||
|
||||
Object.values(lastItem.customProperties).some(prop => prop.trim());
|
||||
|
||||
if (lastItemHasContent) {
|
||||
const newItem: Item = {
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
description: '',
|
||||
wikidataId: undefined,
|
||||
customProperties: {}
|
||||
};
|
||||
newItems.push(newItem);
|
||||
|
||||
// Don't auto-save empty items immediately
|
||||
// They will be saved when user starts typing
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
});
|
||||
}, [saveItemMutation]);
|
||||
}, [saveItemMutation, queryClient, url]);
|
||||
|
||||
const removeItem = useCallback((index: number) => {
|
||||
const itemId = items[index].id;
|
||||
|
|
|
@ -39,85 +39,76 @@ async function getOrCreateProperty(tx: TransactionClient, propertyName: string):
|
|||
|
||||
// Get items by URL
|
||||
export async function getItemsByUrl(url: string): Promise<Item[]> {
|
||||
console.log(`[DB] Fetching items for URL: ${url}`);
|
||||
console.log(`[DB] Getting items for URL: ${url}`);
|
||||
|
||||
// First check if URL exists
|
||||
const urlRecord = await prisma.url.findUnique({
|
||||
where: { url }
|
||||
});
|
||||
|
||||
if (!urlRecord) {
|
||||
console.log(`[DB] URL not found: ${url}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[DB] Found URL ID: ${urlRecord.id}`);
|
||||
|
||||
// Get items with their properties, excluding deleted ones
|
||||
const items = await prisma.item.findMany({
|
||||
where: { urlId: urlRecord.id },
|
||||
include: {
|
||||
itemProperties: {
|
||||
include: { property: true },
|
||||
where: {
|
||||
NOT: {
|
||||
AND: [
|
||||
{ globalItemId: { in: [] } }, // This will be populated below
|
||||
{ propertyId: { in: [] } } // This will be populated below
|
||||
]
|
||||
return await withTransaction(async (tx) => {
|
||||
// Get URL record
|
||||
const urlRecord = await tx.url.findUnique({
|
||||
where: { url },
|
||||
include: {
|
||||
items: {
|
||||
orderBy: { itemOrder: 'asc' },
|
||||
include: {
|
||||
// We don't include itemProperties here since we'll fetch them by globalItemId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { itemOrder: 'asc' }
|
||||
});
|
||||
|
||||
// Get deleted properties for this URL to filter them out
|
||||
const deletedProps = await prisma.deletedProperty.findMany({
|
||||
where: { urlId: urlRecord.id }
|
||||
});
|
||||
|
||||
const deletedPropsSet = new Set(
|
||||
deletedProps.map(dp => `${dp.globalItemId}-${dp.propertyId}`)
|
||||
);
|
||||
|
||||
// Transform to Item format
|
||||
const result: Item[] = items.map(item => {
|
||||
const customProperties: Record<string, string> = {};
|
||||
let name = '';
|
||||
let description = '';
|
||||
|
||||
item.itemProperties.forEach(ip => {
|
||||
const key = `${ip.globalItemId}-${ip.propertyId}`;
|
||||
if (deletedPropsSet.has(key)) {
|
||||
return; // Skip deleted properties
|
||||
}
|
||||
|
||||
if (ip.property.name === 'name') {
|
||||
name = ip.value;
|
||||
} else if (ip.property.name === 'description') {
|
||||
description = ip.value;
|
||||
} else {
|
||||
customProperties[ip.property.name] = ip.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name,
|
||||
description,
|
||||
wikidataId: item.wikidataId || undefined,
|
||||
customProperties
|
||||
};
|
||||
if (!urlRecord) {
|
||||
console.log(`[DB] No URL record found for: ${url}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[DB] Found ${urlRecord.items.length} items for URL: ${url}`);
|
||||
|
||||
// For each item, get properties from globalItemId
|
||||
const items: Item[] = [];
|
||||
|
||||
for (const dbItem of urlRecord.items) {
|
||||
// Get all properties for this globalItemId
|
||||
const itemProperties = await tx.itemProperty.findMany({
|
||||
where: { globalItemId: dbItem.globalItemId },
|
||||
include: { property: true }
|
||||
});
|
||||
|
||||
// Separate core properties from custom properties
|
||||
const customProperties: Record<string, string> = {};
|
||||
let name = '';
|
||||
let description = '';
|
||||
|
||||
for (const prop of itemProperties) {
|
||||
const propName = prop.property.name;
|
||||
const propValue = prop.value;
|
||||
|
||||
if (propName === 'name') {
|
||||
name = propValue;
|
||||
} else if (propName === 'description') {
|
||||
description = propValue;
|
||||
} else {
|
||||
customProperties[propName] = propValue;
|
||||
}
|
||||
}
|
||||
|
||||
const item: Item = {
|
||||
id: dbItem.id,
|
||||
name,
|
||||
description,
|
||||
wikidataId: dbItem.wikidataId || undefined,
|
||||
customProperties
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
console.log(`[DB] Returning ${items.length} items with properties`);
|
||||
return items;
|
||||
});
|
||||
|
||||
console.log(`[DB] Fetched ${result.length} items`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Insert item by URL
|
||||
export async function insertItemByUrl(url: string, item: Item): Promise<void> {
|
||||
console.log(`[DB] Starting insert for URL: ${url}, Item: ${item.id}`);
|
||||
console.log(`[DB] Starting insert for URL: ${url}, Item: ${item.id}, Name: ${item.name}`);
|
||||
|
||||
await withTransaction(async (tx) => {
|
||||
// 1. Get or create URL
|
||||
|
@ -129,27 +120,50 @@ export async function insertItemByUrl(url: string, item: Item): Promise<void> {
|
|||
|
||||
console.log(`[DB] URL ID: ${urlRecord.id}`);
|
||||
|
||||
// 2. Get max order for new items
|
||||
// 2. Check if this specific item already exists for this URL
|
||||
const existingItem = await tx.item.findUnique({
|
||||
where: { id: item.id }
|
||||
});
|
||||
|
||||
// 3. Find or create globalItemId based on item name
|
||||
let globalItemId: string;
|
||||
|
||||
if (existingItem) {
|
||||
// Use existing globalItemId if item already exists
|
||||
globalItemId = existingItem.globalItemId;
|
||||
console.log(`[DB] Using existing global item ID: ${globalItemId}`);
|
||||
} else {
|
||||
// For new items, find existing globalItemId by name or create new one
|
||||
const namePropertyId = await getOrCreateProperty(tx, 'name');
|
||||
|
||||
// Look for existing global item with the same name
|
||||
const existingGlobalItem = await tx.itemProperty.findFirst({
|
||||
where: {
|
||||
propertyId: namePropertyId,
|
||||
value: item.name.trim()
|
||||
},
|
||||
select: { globalItemId: true }
|
||||
});
|
||||
|
||||
if (existingGlobalItem && item.name.trim()) {
|
||||
// Use existing globalItemId for items with the same name
|
||||
globalItemId = existingGlobalItem.globalItemId;
|
||||
console.log(`[DB] Found existing global item ID for name "${item.name}": ${globalItemId}`);
|
||||
} else {
|
||||
// Create new globalItemId for new names or empty names
|
||||
globalItemId = uuidv4();
|
||||
console.log(`[DB] Created new global item ID: ${globalItemId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Get max order for new items
|
||||
const maxOrder = await tx.item.findFirst({
|
||||
where: { urlId: urlRecord.id },
|
||||
orderBy: { itemOrder: 'desc' },
|
||||
select: { itemOrder: true }
|
||||
});
|
||||
|
||||
// 3. Check if global item exists (by name)
|
||||
const namePropertyId = await getOrCreateProperty(tx, 'name');
|
||||
const existingGlobalItem = await tx.itemProperty.findFirst({
|
||||
where: {
|
||||
propertyId: namePropertyId,
|
||||
value: item.name
|
||||
},
|
||||
select: { globalItemId: true }
|
||||
});
|
||||
|
||||
const globalItemId = existingGlobalItem?.globalItemId || uuidv4();
|
||||
console.log(`[DB] Using global item ID: ${globalItemId}`);
|
||||
|
||||
// 4. Upsert item
|
||||
// 5. Upsert the item record (unique by item.id)
|
||||
await tx.item.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
|
@ -166,7 +180,7 @@ export async function insertItemByUrl(url: string, item: Item): Promise<void> {
|
|||
}
|
||||
});
|
||||
|
||||
// 5. Handle all properties (core + custom)
|
||||
// 6. Handle all properties (core + custom)
|
||||
const allProperties = [
|
||||
['name', item.name],
|
||||
['description', item.description],
|
||||
|
@ -176,6 +190,7 @@ export async function insertItemByUrl(url: string, item: Item): Promise<void> {
|
|||
for (const [propName, propValue] of allProperties) {
|
||||
const propertyId = await getOrCreateProperty(tx, propName);
|
||||
|
||||
// Update properties for the globalItemId (this affects all items with same name)
|
||||
await tx.itemProperty.upsert({
|
||||
where: {
|
||||
globalItemId_propertyId: {
|
||||
|
@ -192,7 +207,7 @@ export async function insertItemByUrl(url: string, item: Item): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
console.log(`[DB] Item ${item.id} inserted/updated successfully`);
|
||||
console.log(`[DB] Item ${item.id} with global ID ${globalItemId} inserted/updated successfully`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue