Skip to content

Commit 0eda259

Browse files
authored
Merge pull request #33 from objectql/copilot/implement-validator-logic
2 parents 00cc010 + 1895790 commit 0eda259

2 files changed

Lines changed: 487 additions & 13 deletions

File tree

packages/foundation/core/src/validator.ts

Lines changed: 196 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -376,34 +376,217 @@ export class Validator {
376376
}
377377

378378
/**
379-
* Validate uniqueness (stub - requires database access).
379+
* Validate uniqueness by checking database for existing values.
380380
*/
381381
private async validateUniqueness(
382382
rule: UniquenessValidationRule,
383383
context: ValidationContext
384384
): Promise<ValidationRuleResult> {
385-
// TODO: Implement database query for uniqueness check
386-
// This requires access to the data layer (driver/repository)
387-
// Stub: Pass silently until implementation is complete
388-
return {
389-
rule: rule.name,
390-
valid: true,
391-
};
385+
// Check if API is available for database access
386+
if (!context.api) {
387+
// If no API provided, we can't validate - pass by default
388+
return {
389+
rule: rule.name,
390+
valid: true,
391+
};
392+
}
393+
394+
// Get object name from context metadata
395+
if (!context.metadata?.objectName) {
396+
return {
397+
rule: rule.name,
398+
valid: false,
399+
message: 'Object name not provided in validation context',
400+
severity: rule.severity || 'error',
401+
};
402+
}
403+
404+
const objectName = context.metadata.objectName;
405+
406+
// Determine fields to check for uniqueness
407+
const fieldsToCheck: string[] = rule.fields || (rule.field ? [rule.field] : []);
408+
409+
if (fieldsToCheck.length === 0) {
410+
return {
411+
rule: rule.name,
412+
valid: false,
413+
message: 'No fields specified for uniqueness validation',
414+
severity: rule.severity || 'error',
415+
};
416+
}
417+
418+
// Build query filter
419+
const filters: Record<string, any> = {};
420+
421+
// Add field conditions
422+
for (const field of fieldsToCheck) {
423+
const fieldValue = context.record[field];
424+
425+
// Skip validation if field value is null/undefined (no value to check uniqueness for)
426+
if (fieldValue === null || fieldValue === undefined) {
427+
return {
428+
rule: rule.name,
429+
valid: true,
430+
};
431+
}
432+
433+
// Handle case sensitivity for string values
434+
if (typeof fieldValue === 'string' && rule.case_sensitive === false) {
435+
// NOTE: Case-insensitive comparison requires driver-specific implementation
436+
// Some drivers support regex (MongoDB), others use LOWER() function (SQL)
437+
// For now, we use exact match - driver adapters should implement case-insensitive logic
438+
filters[field] = fieldValue;
439+
} else {
440+
filters[field] = fieldValue;
441+
}
442+
}
443+
444+
// Apply scope conditions if specified
445+
if (rule.scope) {
446+
// Evaluate scope condition and add to filters
447+
const scopeFields = this.extractFieldsFromCondition(rule.scope);
448+
for (const field of scopeFields) {
449+
if (context.record[field] !== undefined) {
450+
filters[field] = context.record[field];
451+
}
452+
}
453+
}
454+
455+
// Exclude current record for update operations
456+
if (context.operation === 'update' && context.previousRecord?._id) {
457+
filters._id = { $ne: context.previousRecord._id };
458+
}
459+
460+
try {
461+
// Query database to count existing records with same field values
462+
const count = await context.api.count(objectName, filters);
463+
464+
const valid = count === 0;
465+
466+
return {
467+
rule: rule.name,
468+
valid,
469+
message: valid ? undefined : this.formatMessage(rule.message, context.record),
470+
error_code: rule.error_code,
471+
severity: rule.severity || 'error',
472+
fields: fieldsToCheck,
473+
};
474+
} catch (error) {
475+
// If query fails, treat as validation error
476+
return {
477+
rule: rule.name,
478+
valid: false,
479+
message: `Uniqueness check failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
480+
severity: rule.severity || 'error',
481+
fields: fieldsToCheck,
482+
};
483+
}
392484
}
393485

394486
/**
395-
* Validate business rule (stub - requires complex logic).
487+
* Extract field names from a validation condition.
488+
*/
489+
private extractFieldsFromCondition(condition: ValidationCondition): string[] {
490+
const fields: string[] = [];
491+
492+
if (condition.field) {
493+
fields.push(condition.field);
494+
}
495+
496+
if (condition.all_of) {
497+
for (const subcondition of condition.all_of) {
498+
fields.push(...this.extractFieldsFromCondition(subcondition));
499+
}
500+
}
501+
502+
if (condition.any_of) {
503+
for (const subcondition of condition.any_of) {
504+
fields.push(...this.extractFieldsFromCondition(subcondition));
505+
}
506+
}
507+
508+
return fields;
509+
}
510+
511+
/**
512+
* Validate business rule by evaluating constraint conditions.
396513
*/
397514
private async validateBusinessRule(
398515
rule: BusinessRuleValidationRule,
399516
context: ValidationContext
400517
): Promise<ValidationRuleResult> {
401-
// TODO: Implement business rule evaluation
402-
// This requires expression parsing and relationship resolution
403-
// Stub: Pass silently until implementation is complete
518+
if (!rule.constraint) {
519+
// No constraint specified, validation passes
520+
return {
521+
rule: rule.name,
522+
valid: true,
523+
};
524+
}
525+
526+
const constraint = rule.constraint;
527+
let valid = true;
528+
529+
// Evaluate all_of conditions (all must be true)
530+
if (constraint.all_of && constraint.all_of.length > 0) {
531+
valid = constraint.all_of.every(condition => this.evaluateCondition(condition, context.record));
532+
533+
if (!valid) {
534+
return {
535+
rule: rule.name,
536+
valid: false,
537+
message: this.formatMessage(rule.message, context.record),
538+
error_code: rule.error_code,
539+
severity: rule.severity || 'error',
540+
};
541+
}
542+
}
543+
544+
// Evaluate any_of conditions (at least one must be true)
545+
if (constraint.any_of && constraint.any_of.length > 0) {
546+
valid = constraint.any_of.some(condition => this.evaluateCondition(condition, context.record));
547+
548+
if (!valid) {
549+
return {
550+
rule: rule.name,
551+
valid: false,
552+
message: this.formatMessage(rule.message, context.record),
553+
error_code: rule.error_code,
554+
severity: rule.severity || 'error',
555+
};
556+
}
557+
}
558+
559+
// Evaluate expression if provided (basic implementation)
560+
if (constraint.expression) {
561+
// For now, we'll treat expression validation as a stub
562+
// Full implementation would require safe expression evaluation
563+
// This could be extended to use a safe expression evaluator in the future
564+
valid = true;
565+
}
566+
567+
// Evaluate then_require conditions (conditional required fields)
568+
if (constraint.then_require && constraint.then_require.length > 0) {
569+
for (const condition of constraint.then_require) {
570+
const conditionMet = this.evaluateCondition(condition, context.record);
571+
572+
if (!conditionMet) {
573+
return {
574+
rule: rule.name,
575+
valid: false,
576+
message: this.formatMessage(rule.message, context.record),
577+
error_code: rule.error_code,
578+
severity: rule.severity || 'error',
579+
};
580+
}
581+
}
582+
}
583+
404584
return {
405585
rule: rule.name,
406-
valid: true,
586+
valid,
587+
message: valid ? undefined : this.formatMessage(rule.message, context.record),
588+
error_code: rule.error_code,
589+
severity: rule.severity || 'error',
407590
};
408591
}
409592

0 commit comments

Comments
 (0)