/* |-------------------------------------------------------------------------- | Preloaded File - node ace make:preload rules/orcid | ❯ Do you want to register the preload file in .adonisrc.ts file? (y/N) · true | DONE: create start/rules/orcid.ts | DONE: update adonisrc.ts file |-------------------------------------------------------------------------- */ import vine, { VineString } from '@vinejs/vine'; import { FieldContext } from '@vinejs/vine/types'; /** * ORCID Validator Implementation * * Validates ORCID identifiers using both format validation and checksum verification. * ORCID (Open Researcher and Contributor ID) is a persistent digital identifier * that distinguishes researchers and supports automated linkages between them * and their professional activities. * * Format: 0000-0000-0000-0000 (where the last digit can be X for checksum 10) * Algorithm: MOD-11-2 checksum validation as per ISO/IEC 7064:2003 * * @param value - The ORCID value to validate * @param _options - Unused options parameter (required by VineJS signature) * @param field - VineJS field context for error reporting */ async function orcidValidator(value: unknown, _options: undefined, field: FieldContext) { /** * Type guard: We only validate string values * The "string" rule should handle type validation before this rule runs */ if (typeof value !== 'string') { return; } /** * Handle optional fields: Skip validation for empty strings * This allows the field to be truly optional when used with .optional() */ if (value.trim() === '') { return; } /** * Normalize the ORCID value: * - Remove any whitespace characters * - Convert to uppercase (for potential X check digit) */ const cleanOrcid = value.replace(/\s/g, '').toUpperCase(); /** * Format Validation * * ORCID format regex breakdown: * ^(\d{4}-){3} - Three groups of exactly 4 digits followed by hyphen * \d{3} - Three more digits * [\dX]$ - Final character: either digit or 'X' (for checksum 10) * * Valid examples: 0000-0002-1825-0097, 0000-0002-1825-009X */ const orcidRegex = /^(\d{4}-){3}\d{3}[\dX]$/; if (!orcidRegex.test(cleanOrcid)) { field.report('ORCID must be in format: 0000-0000-0000-0000 or 0000-0000-0000-000X', 'orcid', field); return; } /** * Checksum Validation - MOD-11-2 Algorithm * * This implements the official ORCID checksum algorithm based on ISO/IEC 7064:2003 * to verify mathematical validity and detect typos or invalid identifiers. */ // Step 1: Extract digits and separate check digit const digits = cleanOrcid.replace(/-/g, ''); // Remove hyphens: "0000000218250097" const baseDigits = digits.slice(0, -1); // First 15 digits: "000000021825009" const checkDigit = digits.slice(-1); // Last character: "7" /** * Step 2: Calculate checksum using MOD-11-2 algorithm * * For each digit from left to right: * 1. Add the digit to running total * 2. Multiply result by 2 * * Example for "000000021825009": * - Start with total = 0 * - Process each digit: total = (total + digit) * 2 * - Continue until all 15 digits are processed */ let total = 0; for (const digit of baseDigits) { total = (total + parseInt(digit)) * 2; } /** * Step 3: Calculate expected check digit * * Formula: (12 - (total % 11)) % 11 * - Get remainder when total is divided by 11 * - Subtract from 12 and take modulo 11 again * - If result is 10, use 'X' (since we need single character) * * Example: total = 1314 * - remainder = 1314 % 11 = 5 * - result = (12 - 5) % 11 = 7 * - expectedCheckDigit = "7" */ const remainder = total % 11; const result = (12 - remainder) % 11; const expectedCheckDigit = result === 10 ? 'X' : result.toString(); /** * Step 4: Verify checksum matches * * Compare the actual check digit with the calculated expected value. * If they don't match, the ORCID is invalid (likely contains typos or is fabricated). */ if (checkDigit !== expectedCheckDigit) { field.report('Invalid ORCID checksum', 'orcid', field); return; } // If we reach this point, the ORCID is valid (both format and checksum) } /** * Create the VineJS validation rule * * This creates a reusable rule that can be chained with other VineJS validators */ const orcidRule = vine.createRule(orcidValidator); /** * TypeScript module declaration * * Extends the VineString interface to include our custom orcid() method. * This enables TypeScript autocompletion and type checking when using the rule. */ declare module '@vinejs/vine' { interface VineString { /** * Validates that a string is a valid ORCID identifier * * Checks both format (0000-0000-0000-0000) and mathematical validity * using the MOD-11-2 checksum algorithm. * * @example * ```typescript * // Usage in validation schema * identifier_orcid: vine.string().trim().maxLength(255).orcid().optional() * ``` * * @returns {this} The VineString instance for method chaining */ orcid(): this; } } /** * Register the macro with VineJS * * This adds the .orcid() method to all VineString instances, * allowing it to be used in validation schemas. * * Usage example: * ```typescript * vine.string().orcid().optional() * ``` */ VineString.macro('orcid', function (this: VineString) { return this.use(orcidRule()); });