/** * Plant Database Service * CRUD operations for plants and lineage tracking */ import prisma from './prisma'; import type { Plant, PlantStatus, PropagationType, Prisma } from '@prisma/client'; import type { PaginationOptions, PaginatedResult, LocationFilter, PlantWithLineage } from './types'; import { createPaginatedResult, calculateDistanceKm } from './types'; // Create a new plant export async function createPlant(data: { commonName: string; scientificName?: string; species?: string; genus?: string; family?: string; parentPlantId?: string; propagationType?: PropagationType; generation?: number; plantedDate: Date; status?: PlantStatus; latitude: number; longitude: number; address?: string; city?: string; country?: string; ownerId: string; environment?: Record; growthMetrics?: Record; notes?: string; images?: string[]; plantsNetId?: string; }): Promise { // Determine generation based on parent let generation = data.generation || 0; if (data.parentPlantId && generation === 0) { const parent = await prisma.plant.findUnique({ where: { id: data.parentPlantId }, select: { generation: true }, }); if (parent) { generation = parent.generation + 1; } } return prisma.plant.create({ data: { ...data, generation, propagationType: data.propagationType || 'ORIGINAL', status: data.status || 'SPROUTED', }, }); } // Get plant by ID export async function getPlantById(id: string): Promise { return prisma.plant.findUnique({ where: { id }, }); } // Get plant with owner export async function getPlantWithOwner(id: string) { return prisma.plant.findUnique({ where: { id }, include: { owner: true }, }); } // Get plant with full lineage export async function getPlantWithLineage(id: string): Promise { return prisma.plant.findUnique({ where: { id }, include: { owner: true, parentPlant: true, childPlants: true, }, }); } // Update plant export async function updatePlant( id: string, data: Prisma.PlantUpdateInput ): Promise { return prisma.plant.update({ where: { id }, data, }); } // Update plant status export async function updatePlantStatus( id: string, status: PlantStatus, harvestedDate?: Date ): Promise { return prisma.plant.update({ where: { id }, data: { status, ...(harvestedDate && { harvestedDate }), }, }); } // Delete plant export async function deletePlant(id: string): Promise { return prisma.plant.delete({ where: { id }, }); } // Get plants with pagination export async function getPlants( options: PaginationOptions = {}, filters?: { ownerId?: string; status?: PlantStatus; commonName?: string; species?: string; } ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.PlantWhereInput = {}; if (filters?.ownerId) where.ownerId = filters.ownerId; if (filters?.status) where.status = filters.status; if (filters?.commonName) where.commonName = { contains: filters.commonName, mode: 'insensitive' }; if (filters?.species) where.species = { contains: filters.species, mode: 'insensitive' }; const [plants, total] = await Promise.all([ prisma.plant.findMany({ where, skip, take: limit, orderBy: { registeredAt: 'desc' }, include: { owner: true }, }), prisma.plant.count({ where }), ]); return createPaginatedResult(plants, total, page, limit); } // Get plants by owner export async function getPlantsByOwner(ownerId: string): Promise { return prisma.plant.findMany({ where: { ownerId }, orderBy: { registeredAt: 'desc' }, }); } // Get plants near location export async function getNearbyPlants( location: LocationFilter, excludeOwnerId?: string ): Promise> { // Get all plants and filter by distance // Note: For production, consider using PostGIS for efficient geo queries const plants = await prisma.plant.findMany({ where: excludeOwnerId ? { ownerId: { not: excludeOwnerId } } : undefined, include: { owner: true }, }); const plantsWithDistance = plants .map(plant => ({ ...plant, distance: calculateDistanceKm( location.latitude, location.longitude, plant.latitude, plant.longitude ), })) .filter(plant => plant.distance <= location.radiusKm) .sort((a, b) => a.distance - b.distance); return plantsWithDistance; } // Search plants export async function searchPlants( query: string, options: PaginationOptions = {} ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.PlantWhereInput = { OR: [ { commonName: { contains: query, mode: 'insensitive' } }, { scientificName: { contains: query, mode: 'insensitive' } }, { species: { contains: query, mode: 'insensitive' } }, { genus: { contains: query, mode: 'insensitive' } }, { family: { contains: query, mode: 'insensitive' } }, ], }; const [plants, total] = await Promise.all([ prisma.plant.findMany({ where, skip, take: limit, orderBy: { registeredAt: 'desc' }, include: { owner: true }, }), prisma.plant.count({ where }), ]); return createPaginatedResult(plants, total, page, limit); } // Get full lineage (ancestors and descendants) export async function getPlantLineage(id: string): Promise<{ plant: Plant | null; ancestors: Plant[]; descendants: Plant[]; siblings: Plant[]; }> { const plant = await prisma.plant.findUnique({ where: { id }, include: { parentPlant: true, childPlants: true, }, }); if (!plant) { return { plant: null, ancestors: [], descendants: [], siblings: [] }; } // Get ancestors recursively const ancestors: Plant[] = []; let currentParentId = plant.parentPlantId; while (currentParentId) { const parent = await prisma.plant.findUnique({ where: { id: currentParentId }, }); if (parent) { ancestors.push(parent); currentParentId = parent.parentPlantId; } else { break; } } // Get all descendants recursively const descendants = await getDescendants(id); // Get siblings (other plants from the same parent) let siblings: Plant[] = []; if (plant.parentPlantId) { siblings = await prisma.plant.findMany({ where: { parentPlantId: plant.parentPlantId, id: { not: id }, }, }); } return { plant, ancestors, descendants, siblings, }; } // Helper to get all descendants async function getDescendants(plantId: string): Promise { const children = await prisma.plant.findMany({ where: { parentPlantId: plantId }, }); const allDescendants: Plant[] = [...children]; for (const child of children) { const grandchildren = await getDescendants(child.id); allDescendants.push(...grandchildren); } return allDescendants; } // Clone a plant export async function clonePlant( parentId: string, ownerId: string, propagationType: PropagationType = 'CLONE', overrides?: Partial<{ latitude: number; longitude: number; address: string; city: string; country: string; notes: string; }> ): Promise { const parent = await prisma.plant.findUnique({ where: { id: parentId }, }); if (!parent) { throw new Error('Parent plant not found'); } return prisma.plant.create({ data: { commonName: parent.commonName, scientificName: parent.scientificName, species: parent.species, genus: parent.genus, family: parent.family, parentPlantId: parentId, propagationType, generation: parent.generation + 1, plantedDate: new Date(), status: 'SPROUTED', latitude: overrides?.latitude || parent.latitude, longitude: overrides?.longitude || parent.longitude, address: overrides?.address, city: overrides?.city || parent.city, country: overrides?.country || parent.country, ownerId, notes: overrides?.notes, plantsNetId: parent.plantsNetId, }, }); } // Get plant network statistics export async function getPlantNetworkStats() { const [totalPlants, speciesDistribution, countryDistribution, ownerCount] = await Promise.all([ prisma.plant.count(), prisma.plant.groupBy({ by: ['species'], _count: { species: true }, where: { species: { not: null } }, }), prisma.plant.groupBy({ by: ['country'], _count: { country: true }, where: { country: { not: null } }, }), prisma.user.count({ where: { ownedPlants: { some: {} } }, }), ]); return { totalPlants, totalOwners: ownerCount, species: Object.fromEntries( speciesDistribution.map(s => [s.species || 'unknown', s._count.species]) ), globalDistribution: Object.fromEntries( countryDistribution.map(c => [c.country || 'unknown', c._count.country]) ), }; } // Store blockchain data for a plant export async function updatePlantBlockchain( id: string, blockIndex: number, blockHash: string ): Promise { return prisma.plant.update({ where: { id }, data: { blockIndex, blockHash }, }); }