Files
nowdatabase/frontend/src/shared/validators/reference.ts
T

295 lines
9.6 KiB
TypeScript
Executable File

import { EditDataType, ReferenceDetailsType, ReferenceAuthorType, ReferenceJournalType } from '../types'
import { Validators, validateFields, validator, ValidationError } from './validator'
const authorCheck: (data: ReferenceAuthorType[]) => ValidationError = (data: ReferenceAuthorType[]) => {
if (data.length === 0) {
return 'There must be at least one author'
}
//Check that there's at least one author that's not going to be deleted
let nonRemovedAuthor = false
for (const author of data) {
if (author.rowState && author.rowState !== 'removed') {
nonRemovedAuthor = true
break
}
if (!author.rowState) {
nonRemovedAuthor = true
break
}
}
if (!nonRemovedAuthor) {
return 'There must be at least one author'
}
const errors: string[] = []
for (const author of data) {
if (typeof author.au_num !== 'number' || author.au_num < 0 || author.au_num > data.length) {
const str = 'Something failed with author numbering'
if (!errors.includes(str)) {
errors.push(str)
}
}
if (typeof author.author_initials !== 'string' || author.author_initials.length < 1) {
const str = 'Author initials must be a non-empty string'
if (!errors.includes(str)) {
errors.push(str)
}
}
if (typeof author.author_surname !== 'string' || author.author_surname.length < 1) {
const str = 'Author surname must be a non-empty string'
if (!errors.includes(str)) {
errors.push(str)
}
}
if (typeof author.field_id !== 'number' || author.field_id < 0) {
const str = 'Something failed with setting author field_id'
if (!errors.includes(str)) {
errors.push(str)
}
}
}
if (errors.length > 0) {
return ('Authors gave the following errors: ' + errors.join(', ')) as ValidationError
}
return null as ValidationError
}
const journalCheck: (journal: ReferenceJournalType) => ValidationError = (journal: ReferenceJournalType) => {
if (!journal) {
return 'You must select or create a new journal' as ValidationError
}
//Existing journal can't have rowState 'removed' if journal is mandatory
if (journal.rowState && journal.rowState == 'removed') {
return 'You must select or create a new journal' as ValidationError
}
if (typeof journal.journal_title !== 'string' || journal.journal_title?.length < 1) {
return 'Journal must have a title' as ValidationError
}
return null as ValidationError
}
const dateCheck: (dateString: string) => ValidationError = (dateString: string) => {
// Regular expression to match yyyy-MM-dd format
const regex = /^\d{4}-\d{2}-\d{2}$/
if (!regex.test(dateString)) {
return 'Date must be in the format yyyy-MM-dd'
}
const [year, month, day] = dateString.split('-').map(Number)
if (month < 1 || month > 12) {
return 'Month must be between 01 and 12'
}
const maxDaysInMonth = new Date(year, month, 0).getDate()
if (day < 1 || day > maxDaysInMonth) {
return `Day must be between 01 and ${maxDaysInMonth} for month ${month}`
}
return null
}
const yearCheck: (year: number) => ValidationError = (year: number) => {
if (!Number.isFinite(year)) {
return 'Year must be a valid number'
}
if (!Number.isInteger(year)) {
return 'Year must be a whole number'
}
if (year <= 0) {
return 'Year must be a positive integer'
}
const currentYear = new Date().getFullYear()
if (year > currentYear) {
return `Year cannot be in the future (max ${currentYear})`
}
return null
}
const positiveIntegerCheck: (name: string, num: number) => ValidationError = (name: string, num: number) => {
if (!Number.isFinite(num)) return `${name} must be a valid number`
if (!Number.isInteger(num)) return `${name} must be a whole number`
if (num < 1) return `${name} must be a positive integer`
return null
}
export type ReferenceFieldDisplayNames = Partial<Record<keyof EditDataType<ReferenceDetailsType>, string>>
export type ReferenceDisplayLabelMap = Partial<Record<number, ReferenceFieldDisplayNames>>
type ReferenceValidationOptions = {
displayLabelMap?: ReferenceDisplayLabelMap
}
const getDisplayLabels = (
fields: (keyof EditDataType<ReferenceDetailsType>)[],
refTypeId: number | null,
displayLabelMap?: ReferenceDisplayLabelMap
) => {
if (!refTypeId || !displayLabelMap?.[refTypeId]) return fields.map(field => String(field))
const labelMap = displayLabelMap[refTypeId]
return fields.map(field => labelMap?.[field] ?? String(field))
}
const orCheck = (
data: EditDataType<ReferenceDetailsType>,
fieldName: keyof EditDataType<ReferenceDetailsType>,
options?: ReferenceValidationOptions
): ValidationError => {
let fields: (keyof EditDataType<ReferenceDetailsType>)[] = []
if (data.ref_type_id && data.ref_type_id === 2) {
fields = ['title_primary', 'title_series', 'gen_notes']
}
if (data.ref_type_id && [1, 3, 5, 8, 9, 11, 14].includes(data.ref_type_id)) {
fields = ['title_primary', 'title_secondary', 'title_series', 'gen_notes']
}
if (data.ref_type_id && [4, 7, 12, 13].includes(data.ref_type_id)) {
fields = ['title_primary', 'gen_notes']
}
if (data.ref_type_id && data.ref_type_id === 6) {
fields = ['title_primary', 'title_secondary', 'gen_notes']
}
if (data.ref_type_id && data.ref_type_id === 10) {
fields = ['gen_notes']
}
if (!fields.includes(fieldName)) return null
const hasValue = fields.some(field => {
const value = data[field]
return value != null && typeof value === 'string' && value.length > 0
})
if (!hasValue) {
const displayLabels = getDisplayLabels(fields, data.ref_type_id ?? null, options?.displayLabelMap)
return `At least one of the following fields is required: ${displayLabels.join(', ')}`
}
return null
}
const createReferenceValidators = (
editData: EditDataType<ReferenceDetailsType>,
options?: ReferenceValidationOptions
): Validators<Partial<EditDataType<ReferenceDetailsType>>> => ({
title_primary: {
name: 'title_primary',
useEditData: true,
miscCheck: (obj: object) => {
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_primary', options)
},
},
title_secondary: {
name: 'title_secondary',
useEditData: true,
miscCheck: (obj: object) => {
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_secondary', options)
},
},
title_series: {
name: 'title_series',
useEditData: true,
miscCheck: (obj: object) => {
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'title_series', options)
},
},
gen_notes: {
name: 'gen_notes',
useEditData: true,
miscCheck: (obj: object) => {
return orCheck(obj as EditDataType<ReferenceDetailsType>, 'gen_notes', options)
},
},
ref_type_id: {
name: 'ref_type_id',
required: true,
asNumber: true,
},
date_primary: {
name: 'date_primary',
required: true,
asNumber: yearCheck,
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
return data.ref_type_id != null && ids.includes(data.ref_type_id)
},
},
start_page: {
name: 'start_page',
required: false,
asNumber: (num: number) => positiveIntegerCheck('start_page', num),
},
end_page: {
name: 'end_page',
required: false,
asNumber: (num: number) => {
const base = positiveIntegerCheck('end_page', num)
if (base) return base
if (typeof editData.start_page === 'number' && num < editData.start_page) {
return 'end_page must be greater than or equal to start_page'
}
return null
},
},
date_secondary: {
name: 'date_secondary',
required: false,
asNumber: yearCheck,
},
ref_authors: {
name: 'ref_authors',
required: true,
minLength: 1,
miscArray: authorCheck,
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
const ids: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
return data.ref_type_id != null && ids.includes(data.ref_type_id)
},
},
ref_journal: {
name: 'ref_journal',
miscCheck: journalCheck,
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
const ids: number[] = [1, 5, 14]
return data.ref_type_id != null && ids.includes(data.ref_type_id)
},
},
exact_date: {
name: 'exact_date',
required: true,
asString: dateCheck,
condition: (data: Partial<EditDataType<ReferenceDetailsType>>) => {
const ids: number[] = [6, 7, 10, 11, 12, 13, 14]
return data.ref_type_id != null && ids.includes(data.ref_type_id)
},
},
})
export const validateReference = (
editData: EditDataType<ReferenceDetailsType>,
fieldName: keyof EditDataType<ReferenceDetailsType>,
options?: ReferenceValidationOptions
) => {
const validators = createReferenceValidators(editData, options)
return validator<EditDataType<ReferenceDetailsType>>(validators, editData, fieldName)
}
export const validateReferenceFields = (
editData: EditDataType<ReferenceDetailsType>,
options?: ReferenceValidationOptions
) => validateFields<EditDataType<ReferenceDetailsType>>(createReferenceValidators(editData, options), editData)
export const createReferenceValidatorWithLabels =
(displayLabelMap?: ReferenceDisplayLabelMap) =>
(editData: EditDataType<ReferenceDetailsType>, fieldName: keyof EditDataType<ReferenceDetailsType>) =>
validateReference(editData, fieldName, { displayLabelMap })
export const createReferenceFieldsValidatorWithLabels =
(displayLabelMap?: ReferenceDisplayLabelMap) => (editData: EditDataType<ReferenceDetailsType>) =>
validateReferenceFields(editData, { displayLabelMap })