UNPKG

@codervisor/devlog-core

Version:

Core devlog management functionality

234 lines (233 loc) 8.94 kB
/** * Devlog validation schemas using Zod * * This module provides runtime validation for devlog-related data at the business logic layer. * It ensures data integrity and business rule compliance across all entry points. */ import { z } from 'zod'; /** * Devlog entry validation schema (for save operations) */ export const DevlogEntrySchema = z.object({ id: z.number().int().positive().optional(), key: z.string().optional(), title: z.string().min(1, 'Title is required').max(200, 'Title must be less than 200 characters'), type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']), description: z .string() .min(1, 'Description is required') .max(2000, 'Description must be less than 2000 characters'), status: z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']), priority: z.enum(['low', 'medium', 'high', 'critical']), createdAt: z.string().datetime('Invalid createdAt timestamp'), updatedAt: z.string().datetime('Invalid updatedAt timestamp'), closedAt: z.string().datetime('Invalid closedAt timestamp').nullable().optional(), assignee: z.string().nullable().optional(), archived: z.boolean().optional(), projectId: z.number().int().positive(), acceptanceCriteria: z.array(z.string()).optional(), businessContext: z.string().max(10000, 'Business context too long').nullable().optional(), technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(), notes: z.array(z.any()).optional(), // Notes have their own validation dependencies: z.array(z.any()).optional(), // Dependencies have their own validation }); /** * Devlog creation schema (excludes auto-generated fields) */ export const CreateDevlogEntrySchema = z.object({ key: z.string().optional(), title: z.string().min(1, 'Title is required').max(200, 'Title must be less than 200 characters'), type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']), description: z .string() .min(1, 'Description is required') .max(2000, 'Description must be less than 2000 characters'), status: z .enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']) .default('new'), priority: z.enum(['low', 'medium', 'high', 'critical']).default('medium'), assignee: z.string().nullable().optional(), archived: z.boolean().default(false).optional(), projectId: z.number().int().positive().optional(), acceptanceCriteria: z.array(z.string()).optional(), businessContext: z.string().max(10000, 'Business context too long').nullable().optional(), technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(), }); /** * Devlog update schema (all fields optional except restrictions) */ export const UpdateDevlogEntrySchema = z.object({ key: z.string().optional(), title: z .string() .min(1, 'Title is required') .max(200, 'Title must be less than 200 characters') .optional(), type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']).optional(), description: z .string() .min(1, 'Description is required') .max(2000, 'Description must be less than 2000 characters') .optional(), status: z .enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']) .optional(), priority: z.enum(['low', 'medium', 'high', 'critical']).optional(), assignee: z.string().nullable().optional(), archived: z.boolean().optional(), acceptanceCriteria: z.array(z.string()).optional(), businessContext: z.string().max(10000, 'Business context too long').nullable().optional(), technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(), }); /** * Devlog ID validation schema */ export const DevlogIdSchema = z.number().int().positive('Devlog ID must be a positive integer'); /** * Devlog filter validation schema */ export const DevlogFilterSchema = z.object({ filterType: z .enum([ 'new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled', 'total', 'open', 'closed', ]) .optional(), status: z .array(z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])) .optional(), type: z.array(z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs'])).optional(), priority: z.array(z.enum(['low', 'medium', 'high', 'critical'])).optional(), assignee: z.string().nullable().optional(), fromDate: z.string().datetime().optional(), toDate: z.string().datetime().optional(), search: z.string().optional(), archived: z.boolean().optional(), projectId: z.number().int().positive().optional(), pagination: z .object({ page: z.number().int().min(1).optional(), limit: z.number().int().min(1).max(100).optional(), }) .optional(), }); /** * Validation functions for business logic layer */ export class DevlogValidator { /** * Validate complete devlog entry (for save operations) */ static validateDevlogEntry(data) { const result = DevlogEntrySchema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: result.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`), }; } /** * Validate devlog creation data */ static validateCreateRequest(data) { const result = CreateDevlogEntrySchema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: result.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`), }; } /** * Validate devlog update data */ static validateUpdateRequest(data) { const result = UpdateDevlogEntrySchema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: result.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`), }; } /** * Validate devlog ID */ static validateDevlogId(id) { const result = DevlogIdSchema.safeParse(id); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: [`Invalid devlog ID: ${result.error.errors.map((err) => err.message).join(', ')}`], }; } /** * Validate devlog filter */ static validateFilter(data) { const result = DevlogFilterSchema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } return { success: false, errors: result.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`), }; } /** * Business rule validation - status transition validation */ static validateStatusTransition(currentStatus, newStatus) { // Define valid status transitions const validTransitions = { new: ['in-progress', 'cancelled'], 'in-progress': ['blocked', 'in-review', 'cancelled'], blocked: ['in-progress', 'cancelled'], 'in-review': ['testing', 'in-progress', 'cancelled'], testing: ['done', 'in-progress', 'cancelled'], done: [], // Final state cancelled: [], // Final state }; const allowedTransitions = validTransitions[currentStatus] || []; if (currentStatus === newStatus) { return { success: true }; // No change is always valid } if (!allowedTransitions.includes(newStatus)) { return { success: false, error: `Invalid status transition from '${currentStatus}' to '${newStatus}'. Allowed transitions: ${allowedTransitions.join(', ') || 'none'}`, }; } return { success: true }; } /** * Business rule validation - check for duplicate keys within project */ static async validateUniqueKey(key, projectId, excludeId, checkFunction) { if (!checkFunction) { return { success: true }; // Skip if no check function provided } const isDuplicate = await checkFunction(key, projectId, excludeId); if (isDuplicate) { return { success: false, error: `Devlog with key "${key}" already exists in this project`, }; } return { success: true }; } }