tethys.backend/start/rules/referenceValidation.ts
Arno Kaimbacher b540547e4c
All checks were successful
CI / container-job (push) Successful in 49s
feat: update API controllers, validations, and Vue components
- Modified Api/Authors.Controller.ts to use only personal types and sort by dataset_count.
- Completely rewritten AvatarController.ts.
- Added new Api/CollectionsController.ts for querying collections and collection_roles.
- Modified Api/DatasetController.ts to preload titles, identifier and order by server_date_published.
- Modified FileController.ts to serve files from /storage/app/data/ instead of /storage/app/public.
- Added new Api/UserController for requesting submitters (getSubmitters).
- Improved OaiController.ts with performant DB queries for better ResumptionToken handling.
- Modified Submitter/DatasetController.ts by adding a categorize method for library classification.
- Rewritten ResumptionToken.ts.
- Improved TokenWorkerService.ts to utilize browser fingerprint.
- Edited dataset.ts by adding the doiIdentifier property.
- Enhanced person.ts to improve the fullName property.
- Completely rewritten AsideMenuItem.vue component.
- Updated CarBoxClient.vue to use TypeScript.
- Added new CardBoxDataset.vue for displaying recent datasets on the dashboard.
- Completely rewritten TableSampleClients.vue for the dashboard.
- Completely rewritten UserAvatar.vue.
- Made small layout changes in Dashboard.vue.
- Added new Category.vue for browsing scientific collections.
- Adapted the pinia store in main.ts.
- Added additional routes in start/routes.ts and start/api/routes.ts.
- Improved referenceValidation.ts for better ISBN existence checking.
- NPM dependency updates.
2025-03-14 17:39:58 +01:00

166 lines
6.9 KiB
TypeScript

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<boolean> {
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<boolean> {
// 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<boolean> {
// 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));
});