localgreenchain/lib/db/plants.ts
Claude 3d2ccdc29a
feat(db): implement PostgreSQL database integration with Prisma ORM
Agent 2 - Database Integration (P0 Critical):

- Add Prisma ORM with PostgreSQL for persistent data storage
- Create comprehensive database schema with 20+ models:
  - User & authentication models
  - Plant & lineage tracking
  - Transport events & supply chain
  - Vertical farming (farms, zones, batches, recipes)
  - Demand & market matching
  - Audit logging & blockchain storage

- Implement complete database service layer (lib/db/):
  - users.ts: User CRUD with search and stats
  - plants.ts: Plant operations with lineage tracking
  - transport.ts: Transport events and carbon tracking
  - farms.ts: Vertical farm and crop batch management
  - demand.ts: Consumer preferences and market matching
  - audit.ts: Audit logging and blockchain integrity

- Add PlantChainDB for database-backed blockchain
- Create development seed script with sample data
- Add database documentation (docs/DATABASE.md)
- Update package.json with Prisma dependencies and scripts

This provides the foundation for all other agents to build upon
with persistent, scalable data storage.
2025-11-23 03:56:40 +00:00

378 lines
9.4 KiB
TypeScript

/**
* 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<string, unknown>;
growthMetrics?: Record<string, unknown>;
notes?: string;
images?: string[];
plantsNetId?: string;
}): Promise<Plant> {
// 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<Plant | null> {
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<PlantWithLineage | null> {
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<Plant> {
return prisma.plant.update({
where: { id },
data,
});
}
// Update plant status
export async function updatePlantStatus(
id: string,
status: PlantStatus,
harvestedDate?: Date
): Promise<Plant> {
return prisma.plant.update({
where: { id },
data: {
status,
...(harvestedDate && { harvestedDate }),
},
});
}
// Delete plant
export async function deletePlant(id: string): Promise<Plant> {
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<PaginatedResult<Plant>> {
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<Plant[]> {
return prisma.plant.findMany({
where: { ownerId },
orderBy: { registeredAt: 'desc' },
});
}
// Get plants near location
export async function getNearbyPlants(
location: LocationFilter,
excludeOwnerId?: string
): Promise<Array<Plant & { distance: number }>> {
// 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<PaginatedResult<Plant>> {
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<Plant[]> {
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<Plant> {
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<Plant> {
return prisma.plant.update({
where: { id },
data: { blockIndex, blockHash },
});
}