import { prisma, withTransaction } from './prisma'; import { Item} from '../types/database'; import { v4 as uuidv4 } from 'uuid'; import { Prisma } from '../generated/prisma'; // Type for transaction client type TransactionClient = Prisma.TransactionClient; // Insert URL export async function insertUrl(url: string): Promise { console.log(`[DB] Inserting URL: ${url}`); const result = await prisma.url.upsert({ where: { url }, update: {}, create: { url }, }); console.log(`[DB] URL inserted/found with ID: ${result.id}`); return result.id; } // Get or create property async function getOrCreateProperty(tx: TransactionClient, propertyName: string): Promise { const existing = await tx.property.findUnique({ where: { name: propertyName } }); if (existing) { return existing.id; } const created = await tx.property.create({ data: { name: propertyName, globalUsageCount: 0 } }); return created.id; } // Get items by URL export async function getItemsByUrl(url: string): Promise { console.log(`[DB] Getting items for URL: ${url}`); 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 } } } }); 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 = {}; 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; }); } // Insert item by URL export async function insertItemByUrl(url: string, item: Item): Promise { console.log(`[DB] Starting insert for URL: ${url}, Item: ${item.id}, Name: ${item.name}`); await withTransaction(async (tx) => { // 1. Get or create URL const urlRecord = await tx.url.upsert({ where: { url }, update: {}, create: { url }, }); console.log(`[DB] URL ID: ${urlRecord.id}`); // 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 } }); // 5. Upsert the item record (unique by item.id) await tx.item.upsert({ where: { id: item.id }, update: { urlId: urlRecord.id, wikidataId: item.wikidataId, globalItemId }, create: { id: item.id, urlId: urlRecord.id, wikidataId: item.wikidataId, itemOrder: (maxOrder?.itemOrder || 0) + 1, globalItemId } }); // 6. Handle all properties (core + custom) const allProperties = [ ['name', item.name], ['description', item.description], ...Object.entries(item.customProperties) ]; 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: { globalItemId, propertyId } }, update: { value: propValue }, create: { globalItemId, propertyId, value: propValue } }); } console.log(`[DB] Item ${item.id} with global ID ${globalItemId} inserted/updated successfully`); }); } // Delete item by URL export async function deleteItemByUrl(url: string, itemId: string): Promise { console.log(`[DB] Deleting item ${itemId} from URL: ${url}`); await withTransaction(async (tx) => { // Get URL ID const urlRecord = await tx.url.findUnique({ where: { url } }); if (!urlRecord) { throw new Error(`URL not found: ${url}`); } // Get the item to find its global_item_id const item = await tx.item.findFirst({ where: { id: itemId, urlId: urlRecord.id } }); if (!item) { throw new Error(`Item not found: ${itemId}`); } // Delete the item (this will cascade to item_properties via foreign key) await tx.item.delete({ where: { id: itemId } }); // Clean up item properties for this global item await tx.itemProperty.deleteMany({ where: { globalItemId: item.globalItemId } }); console.log(`[DB] Item ${itemId} deleted successfully`); }); } // Delete property by URL export async function deletePropertyByUrl(url: string, propertyName: string): Promise { console.log(`[DB] Starting soft delete of property ${propertyName} for URL: ${url}`); await withTransaction(async (tx) => { // Get URL ID const urlRecord = await tx.url.findUnique({ where: { url } }); if (!urlRecord) { console.error(`[DB] URL not found: ${url}`); throw new Error(`URL not found: ${url}`); } console.log(`[DB] Found URL record with ID: ${urlRecord.id}`); // Get property ID const property = await tx.property.findUnique({ where: { name: propertyName } }); if (!property) { console.error(`[DB] Property not found: ${propertyName}`); throw new Error(`Property not found: ${propertyName}`); } console.log(`[DB] Found property record with ID: ${property.id}`); // Remove from selected properties first const deletedSelectedProperties = await tx.selectedProperty.deleteMany({ where: { urlId: urlRecord.id, propertyId: property.id } }); console.log(`[DB] Removed ${deletedSelectedProperties.count} selected property records`); // Get all global item IDs for this URL const items = await tx.item.findMany({ where: { urlId: urlRecord.id }, select: { globalItemId: true } }); console.log(`[DB] Found ${items.length} items for URL`); // Insert into deleted_properties for each global item let deletedPropertiesCount = 0; for (const item of items) { await tx.deletedProperty.upsert({ where: { urlId_globalItemId_propertyId: { urlId: urlRecord.id, globalItemId: item.globalItemId, propertyId: property.id } }, update: {}, create: { urlId: urlRecord.id, globalItemId: item.globalItemId, propertyId: property.id } }); deletedPropertiesCount++; } console.log(`[DB] Created/updated ${deletedPropertiesCount} deleted property records`); console.log(`[DB] Successfully soft deleted property ${propertyName} for URL: ${url}`); }); } // Add selected property export async function addSelectedProperty(url: string, propertyName: string): Promise { console.log(`[DB] Adding selected property ${propertyName} for URL: ${url}`); await withTransaction(async (tx) => { // Get or create URL const urlRecord = await tx.url.upsert({ where: { url }, update: {}, create: { url } }); // Get or create property const propertyId = await getOrCreateProperty(tx, propertyName); // Add to selected properties await tx.selectedProperty.upsert({ where: { urlId_propertyId: { urlId: urlRecord.id, propertyId } }, update: {}, create: { urlId: urlRecord.id, propertyId } }); console.log(`[DB] Selected property ${propertyName} added for URL: ${url}`); }); } // Get selected properties export async function getSelectedProperties(url: string): Promise { console.log(`[DB] Getting selected properties for URL: ${url}`); const result = await prisma.selectedProperty.findMany({ where: { url: { url } }, include: { property: true } }); const properties = result.map(sp => sp.property.name); console.log(`[DB] Found ${properties.length} selected properties`); return properties; }