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] Fetching 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 ] } } } }, 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 = {}; 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 }; }); console.log(`[DB] Fetched ${result.length} items`); return result; } // Insert item by URL export async function insertItemByUrl(url: string, item: Item): Promise { console.log(`[DB] Starting insert for URL: ${url}, Item: ${item.id}`); 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. 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 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 } }); // 5. 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); await tx.itemProperty.upsert({ where: { globalItemId_propertyId: { globalItemId, propertyId } }, update: { value: propValue }, create: { globalItemId, propertyId, value: propValue } }); } console.log(`[DB] Item ${item.id} 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] Soft deleting property ${propertyName} for 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 property ID const property = await tx.property.findUnique({ where: { name: propertyName } }); if (!property) { throw new Error(`Property not found: ${propertyName}`); } // Get all global item IDs for this URL const items = await tx.item.findMany({ where: { urlId: urlRecord.id }, select: { globalItemId: true } }); // Insert into deleted_properties for each global item 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 } }); } console.log(`[DB] Property ${propertyName} soft deleted 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; }