localgreenchain/lib/blockchain/PlantChainDB.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

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;