338 lines
8.8 KiB
TypeScript
338 lines
8.8 KiB
TypeScript
|
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<number> {
|
||
|
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<number> {
|
||
|
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<Item[]> {
|
||
|
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<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
|
||
|
};
|
||
|
});
|
||
|
|
||
|
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}`);
|
||
|
|
||
|
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<void> {
|
||
|
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<void> {
|
||
|
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<void> {
|
||
|
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<string[]> {
|
||
|
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;
|
||
|
}
|