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.
568 lines
14 KiB
TypeScript
568 lines
14 KiB
TypeScript
/**
|
|
* Vertical Farm Database Service
|
|
* CRUD operations for vertical farms, zones, and crop batches
|
|
*/
|
|
|
|
import prisma from './prisma';
|
|
import type {
|
|
VerticalFarm,
|
|
GrowingZone,
|
|
CropBatch,
|
|
GrowingRecipe,
|
|
FarmStatus,
|
|
ZoneStatus,
|
|
CropBatchStatus,
|
|
Prisma,
|
|
} from '@prisma/client';
|
|
import type { PaginationOptions, PaginatedResult } from './types';
|
|
import { createPaginatedResult } from './types';
|
|
|
|
// ============================================
|
|
// VERTICAL FARM OPERATIONS
|
|
// ============================================
|
|
|
|
// Create a vertical farm
|
|
export async function createVerticalFarm(data: {
|
|
name: string;
|
|
ownerId: string;
|
|
latitude: number;
|
|
longitude: number;
|
|
address: string;
|
|
city: string;
|
|
country: string;
|
|
timezone?: string;
|
|
specs: Record<string, unknown>;
|
|
environmentalControl?: Record<string, unknown>;
|
|
irrigationSystem?: Record<string, unknown>;
|
|
lightingSystem?: Record<string, unknown>;
|
|
nutrientSystem?: Record<string, unknown>;
|
|
automationLevel?: 'MANUAL' | 'SEMI_AUTOMATED' | 'FULLY_AUTOMATED';
|
|
automationSystems?: Record<string, unknown>;
|
|
}): Promise<VerticalFarm> {
|
|
return prisma.verticalFarm.create({
|
|
data: {
|
|
...data,
|
|
timezone: data.timezone || 'UTC',
|
|
automationLevel: data.automationLevel || 'MANUAL',
|
|
status: 'OFFLINE',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get vertical farm by ID
|
|
export async function getVerticalFarmById(id: string): Promise<VerticalFarm | null> {
|
|
return prisma.verticalFarm.findUnique({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get vertical farm with zones
|
|
export async function getVerticalFarmWithZones(id: string) {
|
|
return prisma.verticalFarm.findUnique({
|
|
where: { id },
|
|
include: {
|
|
owner: true,
|
|
zones: true,
|
|
cropBatches: {
|
|
where: { status: { not: 'COMPLETED' } },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Update vertical farm
|
|
export async function updateVerticalFarm(
|
|
id: string,
|
|
data: Prisma.VerticalFarmUpdateInput
|
|
): Promise<VerticalFarm> {
|
|
return prisma.verticalFarm.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
}
|
|
|
|
// Update farm status
|
|
export async function updateFarmStatus(
|
|
id: string,
|
|
status: FarmStatus
|
|
): Promise<VerticalFarm> {
|
|
return prisma.verticalFarm.update({
|
|
where: { id },
|
|
data: { status },
|
|
});
|
|
}
|
|
|
|
// Delete vertical farm
|
|
export async function deleteVerticalFarm(id: string): Promise<VerticalFarm> {
|
|
return prisma.verticalFarm.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get farms with pagination
|
|
export async function getVerticalFarms(
|
|
options: PaginationOptions = {},
|
|
filters?: {
|
|
ownerId?: string;
|
|
status?: FarmStatus;
|
|
city?: string;
|
|
country?: string;
|
|
}
|
|
): Promise<PaginatedResult<VerticalFarm>> {
|
|
const page = options.page || 1;
|
|
const limit = options.limit || 20;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.VerticalFarmWhereInput = {};
|
|
if (filters?.ownerId) where.ownerId = filters.ownerId;
|
|
if (filters?.status) where.status = filters.status;
|
|
if (filters?.city) where.city = { contains: filters.city, mode: 'insensitive' };
|
|
if (filters?.country) where.country = { contains: filters.country, mode: 'insensitive' };
|
|
|
|
const [farms, total] = await Promise.all([
|
|
prisma.verticalFarm.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: {
|
|
owner: true,
|
|
zones: true,
|
|
},
|
|
}),
|
|
prisma.verticalFarm.count({ where }),
|
|
]);
|
|
|
|
return createPaginatedResult(farms, total, page, limit);
|
|
}
|
|
|
|
// Get farms by owner
|
|
export async function getVerticalFarmsByOwner(ownerId: string): Promise<VerticalFarm[]> {
|
|
return prisma.verticalFarm.findMany({
|
|
where: { ownerId },
|
|
include: { zones: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// GROWING ZONE OPERATIONS
|
|
// ============================================
|
|
|
|
// Create a growing zone
|
|
export async function createGrowingZone(data: {
|
|
name: string;
|
|
farmId: string;
|
|
level: number;
|
|
areaSqm: number;
|
|
lengthM?: number;
|
|
widthM?: number;
|
|
growingMethod: 'NFT' | 'DWC' | 'EBB_FLOW' | 'AEROPONICS' | 'VERTICAL_TOWERS' | 'RACK_SYSTEM';
|
|
plantPositions: number;
|
|
environmentTargets?: Record<string, unknown>;
|
|
}): Promise<GrowingZone> {
|
|
return prisma.growingZone.create({
|
|
data: {
|
|
...data,
|
|
status: 'EMPTY',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get growing zone by ID
|
|
export async function getGrowingZoneById(id: string): Promise<GrowingZone | null> {
|
|
return prisma.growingZone.findUnique({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get growing zone with current batch
|
|
export async function getGrowingZoneWithBatch(id: string) {
|
|
return prisma.growingZone.findUnique({
|
|
where: { id },
|
|
include: {
|
|
farm: true,
|
|
cropBatches: {
|
|
where: { status: { not: 'COMPLETED' } },
|
|
take: 1,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Update growing zone
|
|
export async function updateGrowingZone(
|
|
id: string,
|
|
data: Prisma.GrowingZoneUpdateInput
|
|
): Promise<GrowingZone> {
|
|
return prisma.growingZone.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
}
|
|
|
|
// Update zone status
|
|
export async function updateZoneStatus(
|
|
id: string,
|
|
status: ZoneStatus
|
|
): Promise<GrowingZone> {
|
|
return prisma.growingZone.update({
|
|
where: { id },
|
|
data: { status },
|
|
});
|
|
}
|
|
|
|
// Delete growing zone
|
|
export async function deleteGrowingZone(id: string): Promise<GrowingZone> {
|
|
return prisma.growingZone.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get zones by farm
|
|
export async function getGrowingZonesByFarm(farmId: string): Promise<GrowingZone[]> {
|
|
return prisma.growingZone.findMany({
|
|
where: { farmId },
|
|
orderBy: [{ level: 'asc' }, { name: 'asc' }],
|
|
});
|
|
}
|
|
|
|
// Update zone environment readings
|
|
export async function updateZoneEnvironment(
|
|
id: string,
|
|
readings: Record<string, unknown>
|
|
): Promise<GrowingZone> {
|
|
return prisma.growingZone.update({
|
|
where: { id },
|
|
data: { currentEnvironment: readings },
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// CROP BATCH OPERATIONS
|
|
// ============================================
|
|
|
|
// Create a crop batch
|
|
export async function createCropBatch(data: {
|
|
farmId: string;
|
|
zoneId: string;
|
|
cropType: string;
|
|
variety?: string;
|
|
recipeId?: string;
|
|
seedBatchId?: string;
|
|
plantCount: number;
|
|
plantingDate: Date;
|
|
expectedHarvestDate: Date;
|
|
expectedYieldKg: number;
|
|
}): Promise<CropBatch> {
|
|
// Update zone status
|
|
await prisma.growingZone.update({
|
|
where: { id: data.zoneId },
|
|
data: {
|
|
status: 'PLANTED',
|
|
currentCrop: data.cropType,
|
|
plantingDate: data.plantingDate,
|
|
expectedHarvestDate: data.expectedHarvestDate,
|
|
},
|
|
});
|
|
|
|
return prisma.cropBatch.create({
|
|
data: {
|
|
...data,
|
|
currentStage: 'germinating',
|
|
currentDay: 0,
|
|
status: 'GERMINATING',
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get crop batch by ID
|
|
export async function getCropBatchById(id: string): Promise<CropBatch | null> {
|
|
return prisma.cropBatch.findUnique({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get crop batch with details
|
|
export async function getCropBatchWithDetails(id: string) {
|
|
return prisma.cropBatch.findUnique({
|
|
where: { id },
|
|
include: {
|
|
farm: true,
|
|
zone: true,
|
|
recipe: true,
|
|
seedBatch: true,
|
|
plants: true,
|
|
harvestBatches: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Update crop batch
|
|
export async function updateCropBatch(
|
|
id: string,
|
|
data: Prisma.CropBatchUpdateInput
|
|
): Promise<CropBatch> {
|
|
return prisma.cropBatch.update({
|
|
where: { id },
|
|
data,
|
|
});
|
|
}
|
|
|
|
// Update crop batch status
|
|
export async function updateCropBatchStatus(
|
|
id: string,
|
|
status: CropBatchStatus,
|
|
extras?: {
|
|
currentStage?: string;
|
|
currentDay?: number;
|
|
healthScore?: number;
|
|
actualHarvestDate?: Date;
|
|
actualYieldKg?: number;
|
|
qualityGrade?: string;
|
|
}
|
|
): Promise<CropBatch> {
|
|
const batch = await prisma.cropBatch.update({
|
|
where: { id },
|
|
data: { status, ...extras },
|
|
});
|
|
|
|
// Update zone status based on batch status
|
|
if (status === 'COMPLETED' || status === 'FAILED') {
|
|
await prisma.growingZone.update({
|
|
where: { id: batch.zoneId },
|
|
data: {
|
|
status: 'CLEANING',
|
|
currentCrop: null,
|
|
plantingDate: null,
|
|
expectedHarvestDate: null,
|
|
},
|
|
});
|
|
} else if (status === 'HARVESTING') {
|
|
await prisma.growingZone.update({
|
|
where: { id: batch.zoneId },
|
|
data: { status: 'HARVESTING' },
|
|
});
|
|
} else if (status === 'GROWING') {
|
|
await prisma.growingZone.update({
|
|
where: { id: batch.zoneId },
|
|
data: { status: 'GROWING' },
|
|
});
|
|
}
|
|
|
|
return batch;
|
|
}
|
|
|
|
// Delete crop batch
|
|
export async function deleteCropBatch(id: string): Promise<CropBatch> {
|
|
return prisma.cropBatch.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get crop batches with pagination
|
|
export async function getCropBatches(
|
|
options: PaginationOptions = {},
|
|
filters?: {
|
|
farmId?: string;
|
|
zoneId?: string;
|
|
status?: CropBatchStatus;
|
|
cropType?: string;
|
|
}
|
|
): Promise<PaginatedResult<CropBatch>> {
|
|
const page = options.page || 1;
|
|
const limit = options.limit || 20;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.CropBatchWhereInput = {};
|
|
if (filters?.farmId) where.farmId = filters.farmId;
|
|
if (filters?.zoneId) where.zoneId = filters.zoneId;
|
|
if (filters?.status) where.status = filters.status;
|
|
if (filters?.cropType) where.cropType = { contains: filters.cropType, mode: 'insensitive' };
|
|
|
|
const [batches, total] = await Promise.all([
|
|
prisma.cropBatch.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { plantingDate: 'desc' },
|
|
include: {
|
|
zone: true,
|
|
recipe: true,
|
|
},
|
|
}),
|
|
prisma.cropBatch.count({ where }),
|
|
]);
|
|
|
|
return createPaginatedResult(batches, total, page, limit);
|
|
}
|
|
|
|
// Get active batches by farm
|
|
export async function getActiveCropBatchesByFarm(farmId: string): Promise<CropBatch[]> {
|
|
return prisma.cropBatch.findMany({
|
|
where: {
|
|
farmId,
|
|
status: { notIn: ['COMPLETED', 'FAILED'] },
|
|
},
|
|
include: {
|
|
zone: true,
|
|
recipe: true,
|
|
},
|
|
orderBy: { expectedHarvestDate: 'asc' },
|
|
});
|
|
}
|
|
|
|
// Add issue to crop batch
|
|
export async function addCropBatchIssue(
|
|
id: string,
|
|
issue: {
|
|
type: string;
|
|
severity: string;
|
|
description: string;
|
|
affectedPlants: number;
|
|
}
|
|
) {
|
|
const batch = await prisma.cropBatch.findUnique({
|
|
where: { id },
|
|
select: { issues: true },
|
|
});
|
|
|
|
const issues = (batch?.issues as unknown[] || []) as Record<string, unknown>[];
|
|
issues.push({
|
|
id: `issue_${Date.now()}`,
|
|
timestamp: new Date().toISOString(),
|
|
...issue,
|
|
});
|
|
|
|
return prisma.cropBatch.update({
|
|
where: { id },
|
|
data: { issues },
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// GROWING RECIPE OPERATIONS
|
|
// ============================================
|
|
|
|
// Create a growing recipe
|
|
export async function createGrowingRecipe(data: {
|
|
name: string;
|
|
cropType: string;
|
|
variety?: string;
|
|
version?: string;
|
|
stages: Record<string, unknown>[];
|
|
expectedDays: number;
|
|
expectedYieldGrams: number;
|
|
expectedYieldPerSqm?: number;
|
|
requirements?: Record<string, unknown>;
|
|
source?: 'INTERNAL' | 'COMMUNITY' | 'COMMERCIAL';
|
|
author?: string;
|
|
}): Promise<GrowingRecipe> {
|
|
return prisma.growingRecipe.create({
|
|
data: {
|
|
...data,
|
|
version: data.version || '1.0',
|
|
source: data.source || 'INTERNAL',
|
|
timesUsed: 0,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get growing recipe by ID
|
|
export async function getGrowingRecipeById(id: string): Promise<GrowingRecipe | null> {
|
|
return prisma.growingRecipe.findUnique({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// Get recipes by crop type
|
|
export async function getGrowingRecipesByCrop(cropType: string): Promise<GrowingRecipe[]> {
|
|
return prisma.growingRecipe.findMany({
|
|
where: { cropType: { contains: cropType, mode: 'insensitive' } },
|
|
orderBy: { rating: 'desc' },
|
|
});
|
|
}
|
|
|
|
// Increment recipe usage
|
|
export async function incrementRecipeUsage(id: string): Promise<GrowingRecipe> {
|
|
return prisma.growingRecipe.update({
|
|
where: { id },
|
|
data: { timesUsed: { increment: 1 } },
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// RESOURCE & ANALYTICS OPERATIONS
|
|
// ============================================
|
|
|
|
// Record resource usage
|
|
export async function recordResourceUsage(data: {
|
|
farmId: string;
|
|
periodStart: Date;
|
|
periodEnd: Date;
|
|
electricityKwh: number;
|
|
electricityCostUsd?: number;
|
|
renewablePercent?: number;
|
|
peakDemandKw?: number;
|
|
waterUsageL: number;
|
|
waterCostUsd?: number;
|
|
waterRecycledPercent?: number;
|
|
co2UsedKg?: number;
|
|
co2CostUsd?: number;
|
|
nutrientsUsedL?: number;
|
|
nutrientCostUsd?: number;
|
|
}) {
|
|
return prisma.resourceUsage.create({ data });
|
|
}
|
|
|
|
// Get resource usage for farm
|
|
export async function getResourceUsage(farmId: string, periodStart: Date, periodEnd: Date) {
|
|
return prisma.resourceUsage.findMany({
|
|
where: {
|
|
farmId,
|
|
periodStart: { gte: periodStart },
|
|
periodEnd: { lte: periodEnd },
|
|
},
|
|
orderBy: { periodStart: 'asc' },
|
|
});
|
|
}
|
|
|
|
// Record farm analytics
|
|
export async function recordFarmAnalytics(data: {
|
|
farmId: string;
|
|
period: string;
|
|
totalYieldKg: number;
|
|
yieldPerSqmPerYear?: number;
|
|
cropCyclesCompleted: number;
|
|
averageCyclesDays?: number;
|
|
averageQualityScore?: number;
|
|
gradeAPercent?: number;
|
|
wastagePercent?: number;
|
|
cropSuccessRate?: number;
|
|
spaceUtilization?: number;
|
|
laborHoursPerKg?: number;
|
|
revenueUsd?: number;
|
|
costUsd?: number;
|
|
profitMarginPercent?: number;
|
|
revenuePerSqm?: number;
|
|
carbonFootprintKgPerKg?: number;
|
|
waterUseLPerKg?: number;
|
|
energyUseKwhPerKg?: number;
|
|
topCropsByYield?: Record<string, unknown>[];
|
|
topCropsByRevenue?: Record<string, unknown>[];
|
|
topCropsByEfficiency?: Record<string, unknown>[];
|
|
}) {
|
|
return prisma.farmAnalytics.create({ data });
|
|
}
|
|
|
|
// Get farm analytics
|
|
export async function getFarmAnalytics(farmId: string, period?: string) {
|
|
const where: Prisma.FarmAnalyticsWhereInput = { farmId };
|
|
if (period) where.period = period;
|
|
|
|
return prisma.farmAnalytics.findMany({
|
|
where,
|
|
orderBy: { generatedAt: 'desc' },
|
|
take: period ? 1 : 12,
|
|
});
|
|
}
|