import { FieldContext } from '@vinejs/vine/types'; import vine from '@vinejs/vine'; import { VineString } from '@vinejs/vine'; import { default as axios } from 'axios'; import { ReferenceIdentifierTypes } from '#contracts/enums'; type Options = { typeField: string; }; // Function to check if DOI exists using the DOI API async function checkDoiExists(doi: string): Promise { try { const response = await axios.get(`${doi}`); return response.status === 200; // If status is 200, DOI is valid } catch (error) { return false; // If request fails, DOI does not exist } } // Function to check if ISBN exists using the Open Library API // async function checkIsbnExists(isbn: string): Promise { // try { // const response = await axios.get(`https://isbnsearch.org/isbn/${isbn}`); // return response.status === 200 && response.data.includes('ISBN'); // Check if response contains ISBN information // } catch (error) { // return false; // If request fails, ISBN does not exist // } // } async function checkIsbnExists(isbn: string): Promise { // Try Open Library first try { const response = await axios.get(`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`); const data = response.data; if (Object.keys(data).length > 0) { return true; } } catch (error) { // If an error occurs, continue to the next API } // Fallback to Google Books API try { const response = await axios.get(`https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`); const data = response.data; if (data.totalItems > 0) { return true; } } catch (error) { // If an error occurs, continue to the next API } // Lastly use the Koha library by scraping HTML try { const response = await axios.get(`https://bibliothek.geosphere.at/cgi-bin/koha/opac-search.pl?idx=nb&q=${isbn}`); const html = response.data; // Check if zero results are explicitly indicated (German or English) if (html.includes('Keine Treffer gefunden!') || html.includes('Your search returned 0 results')) { return false; } // Try to extract the count from German message let match = html.match(/Ihre Suche erzielte\s*(\d+)\s*Treffer/); // If not found, try the English equivalent if (!match) { match = html.match(/Your search returned\s*(\d+)\s*results/); } if (match && match[1]) { const count = parseInt(match[1], 10); return count > 0; } // Fallback: if no match is found, return false return false; } catch (error) { return false; } } async function validateReference(value: unknown, options: Options, field: FieldContext) { if (typeof value !== 'string') { return; } const type = field.parent[options.typeField]; if (type === ReferenceIdentifierTypes.URL) { if (!/^https?:\/\/[^\s$.?#].[^\s]*$/.test(value)) { field.report('The {{ field }} must be a valid URL', 'validateReference', field); } else { try { const exists = await checkDoiExists(value); if (!exists) { field.report('The {{ field }} must be an existing URL', 'validateReference', field); } } catch (error) { field.report('Error checking URL existence: ' + error.message, 'validateReference', field); } } } // Check if the value does not match the DOI pattern // The regex pattern ^10.\d{4,9}\/[-._;()/:a-zA-Z0-9]+$ is designed to match valid DOI formats. // - ^10. ensures that the string starts with "10." // - \d{4,9} matches a sequence of 4 to 9 digits. // - \/ matches the literal forward slash character. // - [-._;()/:a-zA-Z0-9]+ matches one or more characters that can include uppercase and lowercase letters, digits, and a set of special characters. // The i flag at the end of the regex makes the matching case-insensitive, meaning it will match both uppercase and lowercase letters. // If the value does not match this pattern, the code inside the if block will execute. else if (type === ReferenceIdentifierTypes.DOI) { // Extract the DOI from the URL if it starts with 'https://doi.org/' const doiPattern = /^https:\/\/doi\.org\/(10.\d{4,9}\/[-._;()/:a-zA-Z0-9]+)$/i; const match = value.match(doiPattern); const doi = match ? match[1] : value; // Check if the extracted DOI or the value itself matches the DOI patter if (!/^10.\d{4,9}\/[-._;()/:a-zA-Z0-9]+$/i.test(doi)) { field.report('The {{ field }} must be a valid DOI', 'validateReference', field); } else { try { const exists = await checkDoiExists(value); if (!exists) { field.report('The {{ field }} must be an existing DOI', 'validateReference', field); } } catch (error) { field.report('Error checking DOI existence: ' + error.message, 'validateReference', field); } } } else if (type === ReferenceIdentifierTypes.ISBN) { const isbnRegex = /^(?:\d{1,5}-\d{1,7}-\d{1,7}-[\dX]$|97[89]-\d{1,5}-\d{1,7}-\d{1,7}-\d)$/; if (!isbnRegex.test(value)) { field.report('Invalid {{ field }}. Expected format: 978-3-16-148410-0 or similar.', 'validateReference', field); } else { try { const exists = await checkIsbnExists(value); if (!exists) { field.report('The {{ field }} must be an existing ISBN', 'validateReference', field); } } catch (error) { field.report('Error checking ISBN existence: ' + error.message, 'validateReference', field); } } } else if (type === ReferenceIdentifierTypes.Handle && !/^\d{2,}.\d{4,9}\/[-._;()/:a-zA-Z0-9]+$/.test(value)) { /// Extract the Handle from the URL if it contains '/handle/' field.report('The {{ field }} must be a valid Handle', 'validateReference', field); } else if ( type === ReferenceIdentifierTypes.URN && !/^urn:[a-zA-Z0-9][a-zA-Z0-9-]{0,31}:[a-zA-Z0-9()+,\-.:=@;$_!*'%/?#]+$/.test(value) ) { field.report('The {{ field }} must be a valid URN', 'validateReference', field); } else if (type === ReferenceIdentifierTypes.ISSN && !/^\d{4}-\d{3}[\dxX]$/.test(value)) { field.report('The {{ field }} must be a valid ISSN', 'validateReference', field); } } export const validateReferenceRule = vine.createRule(validateReference); declare module '@vinejs/vine' { interface VineString { validateReference(options: Options): this; } } VineString.macro('validateReference', function (this: VineString, options: Options) { return this.use(validateReferenceRule(options)); });