defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
252 lines (226 loc) • 7.51 kB
text/typescript
import type { DeFarmConfig, Asset, ValidationResult, ValidationError } from '../types';
/**
* Service for validating assets according to DeFarm rules
*/
export class ValidationService {
private config: DeFarmConfig;
constructor(config: DeFarmConfig) {
this.config = config;
}
async validateAsset(asset: Asset): Promise<ValidationResult> {
const errors: ValidationError[] = [];
const warnings: any[] = [];
// Core validations
this.validateRequiredFields(asset, errors);
this.validateDataTypes(asset, errors);
this.validateBusinessRules(asset, errors, warnings);
// Security validations if enabled
if (this.config.security.dataClassification) {
this.validateDataClassification(asset, errors);
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
warnings: warnings.length > 0 ? warnings : undefined
};
}
private validateRequiredFields(asset: Asset, errors: ValidationError[]): void {
// Name validation
if (!asset.name || asset.name.trim().length === 0) {
errors.push({
field: 'name',
message: 'Asset name is required',
code: 'REQUIRED_FIELD'
});
}
// Category validation
const validCategories = ['livestock', 'crops', 'aquaculture', 'forestry'];
if (!asset.category || !validCategories.includes(asset.category)) {
errors.push({
field: 'category',
message: 'Valid category is required',
code: 'INVALID_CATEGORY'
});
}
// Ownership validation
if (!asset.ownership) {
errors.push({
field: 'ownership',
message: 'Ownership information is required',
code: 'REQUIRED_FIELD'
});
} else {
if (!asset.ownership.ownerId) {
errors.push({
field: 'ownership.ownerId',
message: 'Owner ID is required',
code: 'REQUIRED_FIELD'
});
}
if (!asset.ownership.ownerName) {
errors.push({
field: 'ownership.ownerName',
message: 'Owner name is required',
code: 'REQUIRED_FIELD'
});
}
}
// Metadata validation
if (!asset.metadata) {
errors.push({
field: 'metadata',
message: 'Metadata is required',
code: 'REQUIRED_FIELD'
});
} else {
if (!asset.metadata.source) {
errors.push({
field: 'metadata.source',
message: 'Source is required',
code: 'REQUIRED_FIELD'
});
}
if (asset.metadata.fromProducer === undefined) {
errors.push({
field: 'metadata.fromProducer',
message: 'From producer flag is required',
code: 'REQUIRED_FIELD'
});
}
}
}
private validateDataTypes(asset: Asset, errors: ValidationError[]): void {
// Validate metadata types
if (asset.metadata) {
const validSources = ['producer', 'authority', 'enterprise', 'association'];
if (asset.metadata.source && !validSources.includes(asset.metadata.source)) {
errors.push({
field: 'metadata.source',
message: 'Invalid source type',
code: 'INVALID_VALUE'
});
}
const validVisibility = ['public', 'private', 'partners'];
if (asset.metadata.visibility && !validVisibility.includes(asset.metadata.visibility)) {
errors.push({
field: 'metadata.visibility',
message: 'Invalid visibility type',
code: 'INVALID_VALUE'
});
}
}
// Validate location coordinates if present
if (asset.ownership?.location?.coordinates) {
const [lng, lat] = asset.ownership.location.coordinates;
if (typeof lng !== 'number' || typeof lat !== 'number') {
errors.push({
field: 'ownership.location.coordinates',
message: 'Coordinates must be numbers',
code: 'INVALID_TYPE'
});
} else if (lng < -180 || lng > 180 || lat < -90 || lat > 90) {
errors.push({
field: 'ownership.location.coordinates',
message: 'Invalid coordinate values',
code: 'INVALID_VALUE'
});
}
}
}
private validateBusinessRules(asset: Asset, errors: ValidationError[], warnings: any[]): void {
// Validate source and fromProducer consistency
if (asset.metadata?.source === 'producer' && !asset.metadata.fromProducer) {
warnings.push({
field: 'metadata.fromProducer',
message: 'Source is producer but fromProducer is false',
suggestion: 'Set fromProducer to true for producer-sourced assets'
});
}
// Validate location requirements for certain categories
if (asset.category === 'livestock' && !asset.ownership?.location?.country) {
warnings.push({
field: 'ownership.location.country',
message: 'Country is recommended for livestock traceability',
suggestion: 'Add country information for better traceability'
});
}
// Validate DFID format if present
if (asset.dfid) {
const dfidPattern = /^DF[A-Z]{2}[A-Z0-9]{10,20}$/;
if (!dfidPattern.test(asset.dfid)) {
errors.push({
field: 'dfid',
message: 'Invalid DFID format',
code: 'INVALID_FORMAT'
});
}
}
}
private validateDataClassification(asset: Asset, errors: ValidationError[]): void {
// Ensure sensitive data is properly marked
if (asset.metadata?.visibility === 'public') {
// Check for potentially sensitive fields in public data
const sensitiveFields = [
'personal_id',
'tax_id',
'bank_account',
'phone_number',
'email'
];
for (const field of sensitiveFields) {
if (asset.data?.[field]) {
errors.push({
field: `data.${field}`,
message: 'Sensitive data cannot be public',
code: 'DATA_CLASSIFICATION_ERROR'
});
}
}
}
// Validate encryption requirements
if (this.config.security.encryptionEnabled && asset.metadata?.visibility === 'private') {
if (!asset.data?._encrypted) {
errors.push({
field: 'data',
message: 'Private data must be encrypted',
code: 'ENCRYPTION_REQUIRED'
});
}
}
}
/**
* Validate a batch of assets
*/
async validateBatch(assets: Asset[]): Promise<Map<number, ValidationResult>> {
const results = new Map<number, ValidationResult>();
await Promise.all(
assets.map(async (asset, index) => {
const result = await this.validateAsset(asset);
results.set(index, result);
})
);
return results;
}
/**
* Check if an asset meets minimum quality standards
*/
isHighQuality(asset: Asset): boolean {
// Asset must have certain fields to be considered high quality
const hasDetailedLocation =
asset.ownership?.location?.country &&
asset.ownership?.location?.state &&
asset.ownership?.location?.coordinates;
const hasIdentifiers =
asset.dfid ||
asset.data?.ear_tag ||
asset.data?.rfid ||
asset.data?.batch_number;
const hasCompleteMetadata =
asset.metadata?.source &&
asset.metadata?.fromProducer !== undefined &&
asset.metadata?.visibility &&
asset.metadata?.created &&
asset.metadata?.version;
return !!(hasDetailedLocation && hasIdentifiers && hasCompleteMetadata);
}
}