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.
419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
/**
|
|
* PlantChainDB - Database-backed blockchain for plant lineage tracking
|
|
* This implementation uses PostgreSQL via Prisma for persistence while
|
|
* maintaining blockchain data integrity guarantees
|
|
*/
|
|
|
|
import { PlantBlock } from './PlantBlock';
|
|
import type { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types';
|
|
import * as db from '../db';
|
|
import type { Plant, BlockchainBlock } from '../db/types';
|
|
|
|
/**
|
|
* Convert database Plant model to PlantData format
|
|
*/
|
|
function plantToPlantData(plant: Plant & { owner?: { id: string; name: string; email: string } }): PlantData {
|
|
return {
|
|
id: plant.id,
|
|
commonName: plant.commonName,
|
|
scientificName: plant.scientificName || undefined,
|
|
species: plant.species || undefined,
|
|
genus: plant.genus || undefined,
|
|
family: plant.family || undefined,
|
|
parentPlantId: plant.parentPlantId || undefined,
|
|
propagationType: plant.propagationType.toLowerCase() as PlantData['propagationType'],
|
|
generation: plant.generation,
|
|
plantedDate: plant.plantedDate.toISOString(),
|
|
harvestedDate: plant.harvestedDate?.toISOString(),
|
|
status: plant.status.toLowerCase() as PlantData['status'],
|
|
location: {
|
|
latitude: plant.latitude,
|
|
longitude: plant.longitude,
|
|
address: plant.address || undefined,
|
|
city: plant.city || undefined,
|
|
country: plant.country || undefined,
|
|
},
|
|
owner: plant.owner ? {
|
|
id: plant.owner.id,
|
|
name: plant.owner.name,
|
|
email: plant.owner.email,
|
|
} : {
|
|
id: plant.ownerId,
|
|
name: 'Unknown',
|
|
email: 'unknown@localgreenchain.io',
|
|
},
|
|
childPlants: [], // Will be populated separately if needed
|
|
environment: plant.environment as PlantData['environment'],
|
|
growthMetrics: plant.growthMetrics as PlantData['growthMetrics'],
|
|
notes: plant.notes || undefined,
|
|
images: plant.images || undefined,
|
|
plantsNetId: plant.plantsNetId || undefined,
|
|
registeredAt: plant.registeredAt.toISOString(),
|
|
updatedAt: plant.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* PlantChainDB - Database-backed plant blockchain
|
|
*/
|
|
export class PlantChainDB {
|
|
public difficulty: number;
|
|
|
|
constructor(difficulty: number = 4) {
|
|
this.difficulty = difficulty;
|
|
}
|
|
|
|
/**
|
|
* Initialize the chain with genesis block if needed
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
const latestBlock = await db.getLatestBlockchainBlock();
|
|
if (!latestBlock) {
|
|
await this.createGenesisBlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the genesis block
|
|
*/
|
|
private async createGenesisBlock(): Promise<BlockchainBlock> {
|
|
const genesisPlant: PlantData = {
|
|
id: 'genesis-plant-0',
|
|
commonName: 'Genesis Plant',
|
|
scientificName: 'Blockchain primordialis',
|
|
propagationType: 'original',
|
|
generation: 0,
|
|
plantedDate: new Date().toISOString(),
|
|
status: 'mature',
|
|
location: {
|
|
latitude: 0,
|
|
longitude: 0,
|
|
address: 'The Beginning',
|
|
},
|
|
owner: {
|
|
id: 'system',
|
|
name: 'LocalGreenChain',
|
|
email: 'system@localgreenchain.org',
|
|
},
|
|
childPlants: [],
|
|
registeredAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const block = new PlantBlock(0, new Date().toISOString(), genesisPlant, '0');
|
|
|
|
return db.createBlockchainBlock({
|
|
index: 0,
|
|
timestamp: new Date(),
|
|
previousHash: '0',
|
|
hash: block.hash,
|
|
nonce: 0,
|
|
blockType: 'genesis',
|
|
content: { plant: genesisPlant },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the latest block
|
|
*/
|
|
async getLatestBlock(): Promise<BlockchainBlock | null> {
|
|
return db.getLatestBlockchainBlock();
|
|
}
|
|
|
|
/**
|
|
* Register a new plant and add to blockchain
|
|
*/
|
|
async registerPlant(plantData: {
|
|
id?: string;
|
|
commonName: string;
|
|
scientificName?: string;
|
|
species?: string;
|
|
genus?: string;
|
|
family?: string;
|
|
plantedDate?: Date;
|
|
status?: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED';
|
|
latitude: number;
|
|
longitude: number;
|
|
address?: string;
|
|
city?: string;
|
|
country?: string;
|
|
ownerId: string;
|
|
notes?: string;
|
|
images?: string[];
|
|
plantsNetId?: string;
|
|
}): Promise<{ plant: Plant; block: BlockchainBlock }> {
|
|
// Create plant in database
|
|
const plant = await db.createPlant({
|
|
...plantData,
|
|
plantedDate: plantData.plantedDate || new Date(),
|
|
propagationType: 'ORIGINAL',
|
|
generation: 0,
|
|
});
|
|
|
|
// Get latest block for previous hash
|
|
const latestBlock = await this.getLatestBlock();
|
|
const previousHash = latestBlock?.hash || '0';
|
|
const nextIndex = (latestBlock?.index ?? -1) + 1;
|
|
|
|
// Create PlantBlock for mining
|
|
const plantDataForBlock = await this.getPlantData(plant.id);
|
|
const newBlock = new PlantBlock(
|
|
nextIndex,
|
|
new Date().toISOString(),
|
|
plantDataForBlock!,
|
|
previousHash
|
|
);
|
|
newBlock.mineBlock(this.difficulty);
|
|
|
|
// Store block in database
|
|
const blockchainBlock = await db.createBlockchainBlock({
|
|
index: nextIndex,
|
|
timestamp: new Date(),
|
|
previousHash,
|
|
hash: newBlock.hash,
|
|
nonce: newBlock.nonce,
|
|
blockType: 'plant',
|
|
plantId: plant.id,
|
|
content: { plant: plantDataForBlock, action: 'REGISTER' },
|
|
});
|
|
|
|
// Update plant with blockchain reference
|
|
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
|
|
|
|
return { plant, block: blockchainBlock };
|
|
}
|
|
|
|
/**
|
|
* Clone a plant and add to blockchain
|
|
*/
|
|
async clonePlant(
|
|
parentPlantId: string,
|
|
ownerId: string,
|
|
propagationType: 'SEED' | 'CLONE' | 'CUTTING' | 'DIVISION' | 'GRAFTING',
|
|
overrides?: {
|
|
latitude?: number;
|
|
longitude?: number;
|
|
address?: string;
|
|
city?: string;
|
|
country?: string;
|
|
notes?: string;
|
|
}
|
|
): Promise<{ plant: Plant; block: BlockchainBlock }> {
|
|
// Create cloned plant in database
|
|
const plant = await db.clonePlant(
|
|
parentPlantId,
|
|
ownerId,
|
|
propagationType,
|
|
overrides
|
|
);
|
|
|
|
// Get latest block for previous hash
|
|
const latestBlock = await this.getLatestBlock();
|
|
const previousHash = latestBlock?.hash || '0';
|
|
const nextIndex = (latestBlock?.index ?? -1) + 1;
|
|
|
|
// Create PlantBlock for mining
|
|
const plantDataForBlock = await this.getPlantData(plant.id);
|
|
const newBlock = new PlantBlock(
|
|
nextIndex,
|
|
new Date().toISOString(),
|
|
plantDataForBlock!,
|
|
previousHash
|
|
);
|
|
newBlock.mineBlock(this.difficulty);
|
|
|
|
// Store block in database
|
|
const blockchainBlock = await db.createBlockchainBlock({
|
|
index: nextIndex,
|
|
timestamp: new Date(),
|
|
previousHash,
|
|
hash: newBlock.hash,
|
|
nonce: newBlock.nonce,
|
|
blockType: 'plant',
|
|
plantId: plant.id,
|
|
content: {
|
|
plant: plantDataForBlock,
|
|
action: 'CLONE',
|
|
parentPlantId,
|
|
},
|
|
});
|
|
|
|
// Update plant with blockchain reference
|
|
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
|
|
|
|
return { plant, block: blockchainBlock };
|
|
}
|
|
|
|
/**
|
|
* Update plant status and add to blockchain
|
|
*/
|
|
async updatePlantStatus(
|
|
plantId: string,
|
|
status: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED',
|
|
harvestedDate?: Date
|
|
): Promise<{ plant: Plant; block: BlockchainBlock }> {
|
|
// Update plant in database
|
|
const plant = await db.updatePlantStatus(plantId, status, harvestedDate);
|
|
|
|
// Get latest block for previous hash
|
|
const latestBlock = await this.getLatestBlock();
|
|
const previousHash = latestBlock?.hash || '0';
|
|
const nextIndex = (latestBlock?.index ?? -1) + 1;
|
|
|
|
// Create PlantBlock for mining
|
|
const plantDataForBlock = await this.getPlantData(plant.id);
|
|
const newBlock = new PlantBlock(
|
|
nextIndex,
|
|
new Date().toISOString(),
|
|
plantDataForBlock!,
|
|
previousHash
|
|
);
|
|
newBlock.mineBlock(this.difficulty);
|
|
|
|
// Store block in database
|
|
const blockchainBlock = await db.createBlockchainBlock({
|
|
index: nextIndex,
|
|
timestamp: new Date(),
|
|
previousHash,
|
|
hash: newBlock.hash,
|
|
nonce: newBlock.nonce,
|
|
blockType: 'plant',
|
|
plantId: plant.id,
|
|
content: {
|
|
plant: plantDataForBlock,
|
|
action: 'UPDATE_STATUS',
|
|
newStatus: status,
|
|
},
|
|
});
|
|
|
|
// Update plant with blockchain reference
|
|
await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash);
|
|
|
|
return { plant, block: blockchainBlock };
|
|
}
|
|
|
|
/**
|
|
* Get plant data in PlantData format
|
|
*/
|
|
async getPlantData(plantId: string): Promise<PlantData | null> {
|
|
const plant = await db.getPlantWithOwner(plantId);
|
|
if (!plant) return null;
|
|
|
|
const plantData = plantToPlantData(plant as Plant & { owner: { id: string; name: string; email: string } });
|
|
|
|
// Get child plant IDs
|
|
const lineage = await db.getPlantLineage(plantId);
|
|
plantData.childPlants = lineage.descendants.map(d => d.id);
|
|
|
|
return plantData;
|
|
}
|
|
|
|
/**
|
|
* Get a plant by ID
|
|
*/
|
|
async getPlant(plantId: string): Promise<Plant | null> {
|
|
return db.getPlantById(plantId);
|
|
}
|
|
|
|
/**
|
|
* Get plant with owner
|
|
*/
|
|
async getPlantWithOwner(plantId: string) {
|
|
return db.getPlantWithOwner(plantId);
|
|
}
|
|
|
|
/**
|
|
* Get complete lineage for a plant
|
|
*/
|
|
async getPlantLineage(plantId: string): Promise<PlantLineage | null> {
|
|
const result = await db.getPlantLineage(plantId);
|
|
if (!result.plant) return null;
|
|
|
|
const plant = result.plant as Plant & { owner: { id: string; name: string; email: string } };
|
|
|
|
return {
|
|
plantId,
|
|
ancestors: result.ancestors.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
|
|
descendants: result.descendants.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
|
|
siblings: result.siblings.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })),
|
|
generation: plant.generation,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Find plants near a location
|
|
*/
|
|
async findNearbyPlants(
|
|
latitude: number,
|
|
longitude: number,
|
|
radiusKm: number = 50,
|
|
excludeOwnerId?: string
|
|
): Promise<NearbyPlant[]> {
|
|
const plants = await db.getNearbyPlants(
|
|
{ latitude, longitude, radiusKm },
|
|
excludeOwnerId
|
|
);
|
|
|
|
return plants.map(p => ({
|
|
plant: plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } }),
|
|
distance: p.distance,
|
|
owner: {
|
|
id: p.ownerId,
|
|
name: 'Unknown',
|
|
email: 'unknown@localgreenchain.io',
|
|
},
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get network statistics
|
|
*/
|
|
async getNetworkStats(): Promise<PlantNetwork> {
|
|
return db.getPlantNetworkStats();
|
|
}
|
|
|
|
/**
|
|
* Validate the blockchain integrity
|
|
*/
|
|
async isChainValid(): Promise<boolean> {
|
|
const result = await db.verifyBlockchainIntegrity();
|
|
return result.isValid;
|
|
}
|
|
|
|
/**
|
|
* Get blockchain statistics
|
|
*/
|
|
async getBlockchainStats() {
|
|
return db.getBlockchainStats();
|
|
}
|
|
|
|
/**
|
|
* Search plants
|
|
*/
|
|
async searchPlants(query: string, options?: { page?: number; limit?: number }) {
|
|
return db.searchPlants(query, options);
|
|
}
|
|
|
|
/**
|
|
* Get plants by owner
|
|
*/
|
|
async getPlantsByOwner(ownerId: string) {
|
|
return db.getPlantsByOwner(ownerId);
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let plantChainInstance: PlantChainDB | null = null;
|
|
|
|
/**
|
|
* Get the singleton PlantChainDB instance
|
|
*/
|
|
export async function getPlantChain(): Promise<PlantChainDB> {
|
|
if (!plantChainInstance) {
|
|
plantChainInstance = new PlantChainDB();
|
|
await plantChainInstance.initialize();
|
|
}
|
|
return plantChainInstance;
|
|
}
|
|
|
|
export default PlantChainDB;
|