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.
296 lines
7.8 KiB
TypeScript
296 lines
7.8 KiB
TypeScript
/**
|
|
* Audit & Blockchain Database Service
|
|
* Operations for audit logging and blockchain block storage
|
|
*/
|
|
|
|
import prisma from './prisma';
|
|
import type { AuditLog, BlockchainBlock, Prisma } from '@prisma/client';
|
|
import type { PaginationOptions, PaginatedResult, DateRangeFilter } from './types';
|
|
import { createPaginatedResult } from './types';
|
|
|
|
// ============================================
|
|
// AUDIT LOG OPERATIONS
|
|
// ============================================
|
|
|
|
// Create an audit log entry
|
|
export async function createAuditLog(data: {
|
|
userId?: string;
|
|
action: string;
|
|
entityType: string;
|
|
entityId?: string;
|
|
previousValue?: Record<string, unknown>;
|
|
newValue?: Record<string, unknown>;
|
|
metadata?: Record<string, unknown>;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
}): Promise<AuditLog> {
|
|
return prisma.auditLog.create({ data });
|
|
}
|
|
|
|
// Get audit logs with pagination
|
|
export async function getAuditLogs(
|
|
options: PaginationOptions = {},
|
|
filters?: {
|
|
userId?: string;
|
|
action?: string;
|
|
entityType?: string;
|
|
entityId?: string;
|
|
dateRange?: DateRangeFilter;
|
|
}
|
|
): Promise<PaginatedResult<AuditLog>> {
|
|
const page = options.page || 1;
|
|
const limit = options.limit || 50;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.AuditLogWhereInput = {};
|
|
if (filters?.userId) where.userId = filters.userId;
|
|
if (filters?.action) where.action = { contains: filters.action, mode: 'insensitive' };
|
|
if (filters?.entityType) where.entityType = filters.entityType;
|
|
if (filters?.entityId) where.entityId = filters.entityId;
|
|
if (filters?.dateRange) {
|
|
where.timestamp = {
|
|
gte: filters.dateRange.start,
|
|
lte: filters.dateRange.end,
|
|
};
|
|
}
|
|
|
|
const [logs, total] = await Promise.all([
|
|
prisma.auditLog.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { timestamp: 'desc' },
|
|
include: { user: true },
|
|
}),
|
|
prisma.auditLog.count({ where }),
|
|
]);
|
|
|
|
return createPaginatedResult(logs, total, page, limit);
|
|
}
|
|
|
|
// Get audit logs for entity
|
|
export async function getEntityAuditLogs(
|
|
entityType: string,
|
|
entityId: string
|
|
): Promise<AuditLog[]> {
|
|
return prisma.auditLog.findMany({
|
|
where: { entityType, entityId },
|
|
orderBy: { timestamp: 'desc' },
|
|
include: { user: true },
|
|
});
|
|
}
|
|
|
|
// Get audit logs by user
|
|
export async function getUserAuditLogs(
|
|
userId: string,
|
|
options: PaginationOptions = {}
|
|
): Promise<PaginatedResult<AuditLog>> {
|
|
const page = options.page || 1;
|
|
const limit = options.limit || 50;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const [logs, total] = await Promise.all([
|
|
prisma.auditLog.findMany({
|
|
where: { userId },
|
|
skip,
|
|
take: limit,
|
|
orderBy: { timestamp: 'desc' },
|
|
}),
|
|
prisma.auditLog.count({ where: { userId } }),
|
|
]);
|
|
|
|
return createPaginatedResult(logs, total, page, limit);
|
|
}
|
|
|
|
// Get recent audit logs summary
|
|
export async function getAuditLogsSummary(hours: number = 24) {
|
|
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
|
|
|
|
const [
|
|
totalLogs,
|
|
actionBreakdown,
|
|
entityBreakdown,
|
|
recentLogs,
|
|
] = await Promise.all([
|
|
prisma.auditLog.count({
|
|
where: { timestamp: { gte: since } },
|
|
}),
|
|
prisma.auditLog.groupBy({
|
|
by: ['action'],
|
|
_count: { action: true },
|
|
where: { timestamp: { gte: since } },
|
|
}),
|
|
prisma.auditLog.groupBy({
|
|
by: ['entityType'],
|
|
_count: { entityType: true },
|
|
where: { timestamp: { gte: since } },
|
|
}),
|
|
prisma.auditLog.findMany({
|
|
where: { timestamp: { gte: since } },
|
|
orderBy: { timestamp: 'desc' },
|
|
take: 10,
|
|
include: { user: true },
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
totalLogs,
|
|
actionBreakdown: Object.fromEntries(
|
|
actionBreakdown.map(a => [a.action, a._count.action])
|
|
),
|
|
entityBreakdown: Object.fromEntries(
|
|
entityBreakdown.map(e => [e.entityType, e._count.entityType])
|
|
),
|
|
recentLogs,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// BLOCKCHAIN BLOCK OPERATIONS
|
|
// ============================================
|
|
|
|
// Create a blockchain block
|
|
export async function createBlockchainBlock(data: {
|
|
index: number;
|
|
timestamp: Date;
|
|
previousHash: string;
|
|
hash: string;
|
|
nonce: number;
|
|
blockType: string;
|
|
plantId?: string;
|
|
transportEventId?: string;
|
|
content: Record<string, unknown>;
|
|
}): Promise<BlockchainBlock> {
|
|
return prisma.blockchainBlock.create({ data });
|
|
}
|
|
|
|
// Get blockchain block by index
|
|
export async function getBlockchainBlockByIndex(index: number): Promise<BlockchainBlock | null> {
|
|
return prisma.blockchainBlock.findUnique({
|
|
where: { index },
|
|
});
|
|
}
|
|
|
|
// Get blockchain block by hash
|
|
export async function getBlockchainBlockByHash(hash: string): Promise<BlockchainBlock | null> {
|
|
return prisma.blockchainBlock.findUnique({
|
|
where: { hash },
|
|
});
|
|
}
|
|
|
|
// Get latest blockchain block
|
|
export async function getLatestBlockchainBlock(): Promise<BlockchainBlock | null> {
|
|
return prisma.blockchainBlock.findFirst({
|
|
orderBy: { index: 'desc' },
|
|
});
|
|
}
|
|
|
|
// Get blockchain blocks with pagination
|
|
export async function getBlockchainBlocks(
|
|
options: PaginationOptions = {},
|
|
filters?: {
|
|
blockType?: string;
|
|
plantId?: string;
|
|
transportEventId?: string;
|
|
}
|
|
): Promise<PaginatedResult<BlockchainBlock>> {
|
|
const page = options.page || 1;
|
|
const limit = options.limit || 50;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.BlockchainBlockWhereInput = {};
|
|
if (filters?.blockType) where.blockType = filters.blockType;
|
|
if (filters?.plantId) where.plantId = filters.plantId;
|
|
if (filters?.transportEventId) where.transportEventId = filters.transportEventId;
|
|
|
|
const [blocks, total] = await Promise.all([
|
|
prisma.blockchainBlock.findMany({
|
|
where,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { index: 'desc' },
|
|
}),
|
|
prisma.blockchainBlock.count({ where }),
|
|
]);
|
|
|
|
return createPaginatedResult(blocks, total, page, limit);
|
|
}
|
|
|
|
// Verify blockchain integrity
|
|
export async function verifyBlockchainIntegrity(): Promise<{
|
|
isValid: boolean;
|
|
invalidBlocks: number[];
|
|
totalBlocks: number;
|
|
}> {
|
|
const blocks = await prisma.blockchainBlock.findMany({
|
|
orderBy: { index: 'asc' },
|
|
});
|
|
|
|
const invalidBlocks: number[] = [];
|
|
|
|
for (let i = 1; i < blocks.length; i++) {
|
|
const currentBlock = blocks[i];
|
|
const previousBlock = blocks[i - 1];
|
|
|
|
// Check if previous hash matches
|
|
if (currentBlock.previousHash !== previousBlock.hash) {
|
|
invalidBlocks.push(currentBlock.index);
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid: invalidBlocks.length === 0,
|
|
invalidBlocks,
|
|
totalBlocks: blocks.length,
|
|
};
|
|
}
|
|
|
|
// Get blockchain statistics
|
|
export async function getBlockchainStats() {
|
|
const [
|
|
totalBlocks,
|
|
blocksByType,
|
|
latestBlock,
|
|
oldestBlock,
|
|
] = await Promise.all([
|
|
prisma.blockchainBlock.count(),
|
|
prisma.blockchainBlock.groupBy({
|
|
by: ['blockType'],
|
|
_count: { blockType: true },
|
|
}),
|
|
prisma.blockchainBlock.findFirst({ orderBy: { index: 'desc' } }),
|
|
prisma.blockchainBlock.findFirst({ orderBy: { index: 'asc' } }),
|
|
]);
|
|
|
|
return {
|
|
totalBlocks,
|
|
blocksByType: Object.fromEntries(
|
|
blocksByType.map(b => [b.blockType, b._count.blockType])
|
|
),
|
|
latestBlockIndex: latestBlock?.index ?? -1,
|
|
latestBlockHash: latestBlock?.hash,
|
|
oldestBlockTimestamp: oldestBlock?.timestamp,
|
|
latestBlockTimestamp: latestBlock?.timestamp,
|
|
};
|
|
}
|
|
|
|
// Helper to log entity changes
|
|
export async function logEntityChange<T extends Record<string, unknown>>(
|
|
userId: string | undefined,
|
|
action: 'CREATE' | 'UPDATE' | 'DELETE',
|
|
entityType: string,
|
|
entityId: string,
|
|
previousValue: T | null,
|
|
newValue: T | null,
|
|
metadata?: Record<string, unknown>
|
|
): Promise<AuditLog> {
|
|
return createAuditLog({
|
|
userId,
|
|
action,
|
|
entityType,
|
|
entityId,
|
|
previousValue: previousValue || undefined,
|
|
newValue: newValue || undefined,
|
|
metadata,
|
|
});
|
|
}
|