@cabbages/memory-pickle-mcp
Version:
MCP server for AI agent project management - 13 tools for session memory and task tracking
383 lines (382 loc) • 16.5 kB
JavaScript
import { ValidationError, InvalidPriorityError, InvalidProgressError } from './errors.js';
/**
* Comprehensive validation utilities for data integrity and schema enforcement
*/
export class ValidationUtils {
// Project validation
static validateProject(project) {
const errors = [];
if (!project.name || typeof project.name !== 'string') {
errors.push('Project name is required and must be a string');
}
else if (project.name.length === 0 || project.name.length > 200) {
errors.push('Project name must be between 1 and 200 characters');
}
if (project.description && typeof project.description !== 'string') {
errors.push('Project description must be a string');
}
else if (project.description && project.description.length > 20000) {
errors.push('Project description cannot exceed 20000 characters');
}
if (project.status && !['planning', 'in_progress', 'blocked', 'completed', 'archived'].includes(project.status)) {
errors.push('Project status must be one of: planning, in_progress, blocked, completed, archived');
}
if (project.completion_percentage !== undefined) {
if (typeof project.completion_percentage !== 'number' ||
project.completion_percentage < 0 ||
project.completion_percentage > 100) {
errors.push('Project completion percentage must be a number between 0 and 100');
}
}
if (project.id && (typeof project.id !== 'string' || project.id.length === 0)) {
errors.push('Project ID must be a non-empty string');
}
return { isValid: errors.length === 0, errors };
}
// Task validation
static validateTask(task) {
const errors = [];
if (!task.title || typeof task.title !== 'string') {
errors.push('Task title is required and must be a string');
}
else if (task.title.length === 0 || task.title.length > 200) {
errors.push('Task title must be between 1 and 200 characters');
}
if (task.description && typeof task.description !== 'string') {
errors.push('Task description must be a string');
}
else if (task.description && task.description.length > 2000) {
errors.push('Task description cannot exceed 2000 characters');
}
if (task.priority && !['low', 'medium', 'high', 'critical'].includes(task.priority)) {
errors.push('Task priority must be one of: low, medium, high, critical');
}
if (!task.project_id || typeof task.project_id !== 'string') {
errors.push('Task project_id is required and must be a string');
}
if (task.parent_id && typeof task.parent_id !== 'string') {
errors.push('Task parent_id must be a string');
}
if (task.progress !== undefined) {
if (typeof task.progress !== 'number' || task.progress < 0 || task.progress > 100) {
errors.push('Task progress must be a number between 0 and 100');
}
}
if (task.completed !== undefined && typeof task.completed !== 'boolean') {
errors.push('Task completed must be a boolean');
}
if (task.notes && !Array.isArray(task.notes)) {
errors.push('Task notes must be an array');
}
else if (task.notes) {
task.notes.forEach((note, index) => {
if (typeof note !== 'string') {
errors.push(`Task note at index ${index} must be a string`);
}
});
}
if (task.blockers && !Array.isArray(task.blockers)) {
errors.push('Task blockers must be an array');
}
else if (task.blockers) {
task.blockers.forEach((blocker, index) => {
if (typeof blocker !== 'string') {
errors.push(`Task blocker at index ${index} must be a string`);
}
});
}
return { isValid: errors.length === 0, errors };
}
// Memory validation
static validateMemory(memory) {
const errors = [];
if (!memory.content || typeof memory.content !== 'string') {
errors.push('Memory content is required and must be a string');
}
else if (memory.content.length === 0 || memory.content.length > 50000) {
errors.push('Memory content must be between 1 and 50000 characters');
}
if (memory.title && typeof memory.title !== 'string') {
errors.push('Memory title must be a string');
}
else if (memory.title && memory.title.length > 500) {
errors.push('Memory title cannot exceed 500 characters');
}
if (memory.importance && !['low', 'medium', 'high', 'critical'].includes(memory.importance)) {
errors.push('Memory importance must be one of: low, medium, high, critical');
}
if (memory.project_id && typeof memory.project_id !== 'string') {
errors.push('Memory project_id must be a string');
}
if (memory.task_id && typeof memory.task_id !== 'string') {
errors.push('Memory task_id must be a string');
}
return { isValid: errors.length === 0, errors };
}
// Database schema validation
static validateDatabaseSchema(database) {
const errors = [];
// Check required top-level properties
if (!database.meta) {
errors.push('Database meta is required');
}
else {
if (!database.meta.version || typeof database.meta.version !== 'string') {
errors.push('Database meta.version is required and must be a string');
}
if (!database.meta.last_updated || typeof database.meta.last_updated !== 'string') {
errors.push('Database meta.last_updated is required and must be a string');
}
if (database.meta.session_count !== undefined && typeof database.meta.session_count !== 'number') {
errors.push('Database meta.session_count must be a number');
}
}
if (!Array.isArray(database.projects)) {
errors.push('Database projects must be an array');
}
else {
database.projects.forEach((project, index) => {
const validation = this.validateProject(project);
if (!validation.isValid) {
errors.push(`Project at index ${index}: ${validation.errors.join(', ')}`);
}
});
}
if (!Array.isArray(database.tasks)) {
errors.push('Database tasks must be an array');
}
else {
database.tasks.forEach((task, index) => {
const validation = this.validateTask(task);
if (!validation.isValid) {
errors.push(`Task at index ${index}: ${validation.errors.join(', ')}`);
}
});
}
if (!Array.isArray(database.memories)) {
errors.push('Database memories must be an array');
}
else {
database.memories.forEach((memory, index) => {
const validation = this.validateMemory(memory);
if (!validation.isValid) {
errors.push(`Memory at index ${index}: ${validation.errors.join(', ')}`);
}
});
}
if (database.templates && typeof database.templates !== 'object') {
errors.push('Database templates must be an object');
}
return { isValid: errors.length === 0, errors };
}
// Input sanitization
/**
* Sanitizes a string by trimming whitespace but preserving special characters
*/
static sanitizeString(input) {
if (typeof input !== 'string') {
return '';
}
// Only trim whitespace, preserve all other characters including newlines, tabs, emojis
return input.trim();
}
static sanitizeId(input) {
if (input === null || input === undefined)
return '';
return String(input).trim();
}
static sanitizeProject(project) {
const sanitized = { ...project };
if (sanitized.name)
sanitized.name = this.sanitizeString(sanitized.name);
if (sanitized.description)
sanitized.description = this.sanitizeString(sanitized.description);
return sanitized;
}
static sanitizeTask(task) {
const sanitized = { ...task };
if (sanitized.title)
sanitized.title = this.sanitizeString(sanitized.title);
if (sanitized.description)
sanitized.description = this.sanitizeString(sanitized.description);
if (sanitized.notes) {
sanitized.notes = sanitized.notes.map(note => this.sanitizeString(note));
}
if (sanitized.blockers) {
sanitized.blockers = sanitized.blockers.map(blocker => this.sanitizeString(blocker));
}
return sanitized;
}
static sanitizeMemory(memory) {
const sanitized = { ...memory };
if (sanitized.title)
sanitized.title = this.sanitizeString(sanitized.title);
if (sanitized.content)
sanitized.content = this.sanitizeString(sanitized.content);
return sanitized;
}
// Data limits validation
static validateDataLimits(database) {
const errors = [];
if (database.projects.length > 5000) {
errors.push('Too many projects - maximum 5000 allowed');
}
if (database.tasks.length > 50000) {
errors.push('Too many tasks - maximum 50000 allowed');
}
if (database.memories.length > 25000) {
errors.push('Too many memories - maximum 25000 allowed');
}
return { isValid: errors.length === 0, errors };
}
// Referential integrity validation
static validateReferentialIntegrity(database) {
const errors = [];
const projectIds = new Set(database.projects.map(p => p.id));
const taskIds = new Set(database.tasks.map(t => t.id));
// Check tasks reference valid projects
database.tasks.forEach(task => {
if (!projectIds.has(task.project_id)) {
errors.push(`Task ${task.id} references non-existent project ${task.project_id}`);
}
if (task.parent_id && !taskIds.has(task.parent_id)) {
errors.push(`Task ${task.id} references non-existent parent task ${task.parent_id}`);
}
});
// Check memories reference valid projects/tasks
database.memories.forEach(memory => {
if (memory.project_id && !projectIds.has(memory.project_id)) {
errors.push(`Memory ${memory.id} references non-existent project ${memory.project_id}`);
}
if (memory.task_id && !taskIds.has(memory.task_id)) {
errors.push(`Memory ${memory.id} references non-existent task ${memory.task_id}`);
}
});
// Check current project reference
if (database.meta.current_project_id && !projectIds.has(database.meta.current_project_id)) {
errors.push(`Current project ${database.meta.current_project_id} does not exist`);
}
return { isValid: errors.length === 0, errors };
}
// Comprehensive database validation
static validateDatabase(database) {
const schemaValidation = this.validateDatabaseSchema(database);
if (!schemaValidation.isValid) {
return schemaValidation;
}
const limitsValidation = this.validateDataLimits(database);
if (!limitsValidation.isValid) {
return limitsValidation;
}
const integrityValidation = this.validateReferentialIntegrity(database);
if (!integrityValidation.isValid) {
return integrityValidation;
}
return { isValid: true, errors: [] };
}
static isValidPriority(priority) {
if (!priority || typeof priority !== 'string')
return false;
return ['critical', 'high', 'medium', 'low'].includes(priority);
}
static isValidStatus(status) {
if (!status || typeof status !== 'string')
return false;
return ['planning', 'in_progress', 'blocked', 'completed', 'archived'].includes(status);
}
static isValidImportance(importance) {
if (!importance || typeof importance !== 'string')
return false;
return ['critical', 'high', 'medium', 'low'].includes(importance);
}
static validateStringLength(value, fieldName, min, max) {
const str = value ? String(value) : '';
if (str.length < min || str.length > max) {
throw new Error(`Field '${fieldName}' must be between ${min} and ${max} characters`);
}
}
static validateProgress(progress) {
if (progress !== undefined && progress !== null) {
if (typeof progress !== 'number' || progress < 0 || progress > 100) {
throw new InvalidProgressError(progress);
}
}
}
static validateArray(value, fieldName, min, max) {
if (!Array.isArray(value)) {
throw new Error(`Field '${fieldName}' must be an array`);
}
if (value.length < min || value.length > max) {
throw new Error(`Field '${fieldName}' must have between ${min} and ${max} items`);
}
}
static validateEnum(value, fieldName, validValues) {
if (!validValues.includes(value)) {
throw new Error(`Field '${fieldName}' must be one of: ${validValues.join(', ')}`);
}
}
static validateOptionalEnum(value, fieldName, validValues) {
if (value !== undefined && value !== null && !validValues.includes(value)) {
throw new Error(`Field '${fieldName}' must be one of: ${validValues.join(', ')}, or undefined`);
}
}
// Missing methods that tests expect
static validatePriority(priority) {
if (priority === undefined || priority === null) {
return; // Allow undefined/null priorities
}
if (typeof priority !== 'string' || !['critical', 'high', 'medium', 'low'].includes(priority)) {
throw new InvalidPriorityError(priority);
}
}
static validateRequiredField(value, fieldName) {
if (value === null || value === undefined) {
throw new ValidationError(fieldName, value, 'cannot be null or undefined');
}
if (typeof value === 'string' && value.trim() === '') {
throw new ValidationError(fieldName, value, 'cannot be empty string');
}
}
static validateLineRange(lineRange) {
if (lineRange === undefined || lineRange === null) {
return; // Allow undefined line ranges
}
if (typeof lineRange !== 'object') {
throw new ValidationError('line_range', lineRange, 'must be an object');
}
if (typeof lineRange.start_line !== 'number' || typeof lineRange.end_line !== 'number') {
throw new ValidationError('line_range', lineRange, 'start_line and end_line must be numbers');
}
if (lineRange.start_line < 1 || lineRange.end_line < 1) {
throw new ValidationError('line_range', lineRange, 'line numbers must be positive (1-based)');
}
if (lineRange.start_line > lineRange.end_line) {
throw new ValidationError('line_range', lineRange, 'start_line must be less than or equal to end_line');
}
}
/**
* Validates email format using a simple regex pattern
*/
static isValidEmail(email) {
if (!email || typeof email !== 'string') {
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* Validates URL format, allowing only http and https protocols
*/
static isValidUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}
try {
const urlObj = new URL(url);
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
}
catch {
return false;
}
}
}