diff --git a/src/functions/createJsonMetadataFromCSV.ts b/src/functions/createJsonMetadataFromCSV.ts index 67b8abc..6da17c3 100644 --- a/src/functions/createJsonMetadataFromCSV.ts +++ b/src/functions/createJsonMetadataFromCSV.ts @@ -37,10 +37,7 @@ export const createJsonMetadataFromCSV = async ({ return { isValid, - errors: { - general: errors.general, - missingAttributes: errors.missingAttributes, - }, + errors, savedJsonFilesLocation, }; }; diff --git a/src/types/jsonMetadataFromCSV.ts b/src/types/jsonMetadataFromCSV.ts index affe53e..e104819 100644 --- a/src/types/jsonMetadataFromCSV.ts +++ b/src/types/jsonMetadataFromCSV.ts @@ -1,8 +1,5 @@ export interface JsonMetadataFromCSVInterface { isValid: boolean; - errors: { - general: string[]; - missingAttributes: string[]; - }; + errors: string[]; savedJsonFilesLocation: string; } diff --git a/src/utils/constants/dictionary.ts b/src/utils/constants/dictionary.ts index 0146ffb..7d49e82 100644 --- a/src/utils/constants/dictionary.ts +++ b/src/utils/constants/dictionary.ts @@ -31,18 +31,15 @@ export const dictionary = { `Error in line number ${line}, column number ${column}. Check if your CSV file is well prepared.`, invalidKeysDetected: (keys: string[]) => `Redundant key(s) detected: ['${keys.join("', '")}']`, csvFileIsEmpty: (path: string) => `No metadata found in CSV file "${getFullSystemPath(path)}".`, - errorInRow: (fileName: string, line: number | string, error: string) => - `Error at: line number ${line} in ${getFullSystemPath(fileName)} - ${error}`, - missingAttributesInRowWithFilePath: (filePath: string, row: number) => - `In file: "${getFullSystemPath(filePath)}" in row ${row}`, - missingAttributes: 'There are missing attributes in the metadata object.', + arrayOfObjectsValidationError: (fileName: string, error: string) => + `Error at: ${getFullSystemPath(fileName)} - ${error}`, imageForNftNotFound: 'Image for NFT not found. The name of the image file should match its corresponding metadata file name (ex: 1.jpg with 1.json) or specify directly the "image" property.', mediaFileNotSupported: 'Media file is not supported.', unsupportedImageMimeType: 'Unsupported image MIME type.', requiredFieldMissing: 'Required field is missing', - requiredTypeFieldIsMissing: 'The required "type" field is missing. ', - requiredAttributeFieldMissing: 'The required "attributes" field is missing. ', + requiredTypeFieldIsMissing: 'The required "type" field is missing.', + requiredAttributeFieldMissing: 'The required "attributes" field is missing.', filePermissionDenied: 'Permission denied', fileEmptyOrFormattingError: 'Unexpected end of JSON input', directoryIsEmpty: 'Directory is empty', diff --git a/src/utils/helpers/validateObjectWithSchema.ts b/src/utils/helpers/validateObjectWithSchema.ts index d69341d..151bc6d 100644 --- a/src/utils/helpers/validateObjectWithSchema.ts +++ b/src/utils/helpers/validateObjectWithSchema.ts @@ -4,6 +4,7 @@ import keys from 'lodash/keys'; import { type z } from 'zod'; import type { ErrorMessageOptions } from 'zod-error'; import { generateErrorMessage } from 'zod-error'; +import { ValidationError } from '../validationError'; export const noPropertiesErrorOptions: ErrorMessageOptions = { prefix: '', @@ -54,7 +55,7 @@ export const validationMetadataErrorOptions: ErrorMessageOptions = { error: '', }, message: { enabled: false }, - transform: ({ errorMessage }) => `${errorMessage} `, + transform: ({ errorMessage }) => errorMessage, }; export const validateObjectWithSchema = ( @@ -64,8 +65,11 @@ export const validateObjectWithSchema = { const validation = Schema.safeParse(object); if (!validation.success) { - const errorMessage = generateErrorMessage(validation.error.issues, errorMessageOptions); - throw new Error(errorMessage); + const errorMessages = validation.error.issues.map((issue) => + generateErrorMessage([issue], errorMessageOptions) + ); + + throw new ValidationError(errorMessages); } const parsedObjectWithSchema = Schema.parse(object); diff --git a/src/utils/services/Hip412Validator.ts b/src/utils/services/Hip412Validator.ts index 3d61578..c09030c 100644 --- a/src/utils/services/Hip412Validator.ts +++ b/src/utils/services/Hip412Validator.ts @@ -7,7 +7,6 @@ import { } from '../validation-schemas/hip412Metadata.schema'; import { validateObjectWithSchema, - noPropertiesErrorOptions, validationMetadataErrorOptions, } from '../helpers/validateObjectWithSchema'; import { errorToMessage } from '../helpers/errorToMessage'; @@ -15,16 +14,12 @@ import { MetadataObject } from '../../types/csv'; import { dictionary } from '../constants/dictionary'; import { getMetadataObjectsForValidation, getNFTsFromToken } from '../../api/mirrorNode'; import { nftMetadataDecoder } from '../helpers/nftMetadataDecoder'; +import { ValidationError } from '../validationError'; interface FileValidationResult { isValid: boolean; fileName?: string; - errors: ValidationErrorsInterface; -} - -interface ValidationErrorsInterface { - general: string[]; - missingAttributes: string[]; + errors: string[]; } interface DirectoryValidationResult { @@ -35,7 +30,6 @@ interface DirectoryValidationResult { interface MetadataError { fileName?: string; general: string[]; - missingAttributes: string[]; } interface MetadataOnChainObjects { @@ -46,52 +40,49 @@ interface MetadataOnChainObjects { export class Hip412Validator { static validateSingleMetadataObject(object: MetadataObject): FileValidationResult { - const errors: ValidationErrorsInterface = { general: [], missingAttributes: [] }; + const errors: string[] = []; try { validateObjectWithSchema(Hip412MetadataSchema, object, validationMetadataErrorOptions); - } catch (error) { - errors.general.push(errorToMessage(error)); - } - - if (!object.attributes) { - errors.missingAttributes.push(dictionary.validation.missingAttributes); + } catch (err) { + if (err instanceof ValidationError) { + errors.push(...err.errors); + } else { + console.error(dictionary.errors.unhandledError); + } } return { - isValid: errors.general.length === 0, + isValid: errors.length === 0, errors, }; } static validateArrayOfObjects = ( metadataObjects: MetadataObject[], - filePath: string + filePath?: string ): FileValidationResult => { - const errors: ValidationErrorsInterface = { general: [], missingAttributes: [] }; + const errors: string[] = []; for (const [index, metadataObject] of metadataObjects.entries()) { try { - validateObjectWithSchema(Hip412MetadataCSVSchema, metadataObject, noPropertiesErrorOptions); + validateObjectWithSchema( + Hip412MetadataCSVSchema, + metadataObject, + validationMetadataErrorOptions + ); } catch (e) { - errors.general.push( - dictionary.validation.errorInRow( - filePath, - index + 1, + errors.push( + dictionary.validation.arrayOfObjectsValidationError( + filePath || `object ${index + 1}`, errorToMessage( errorToMessage(e) === 'Required' ? dictionary.validation.requiredFieldMissing : e ) ) ); } - - if (!metadataObject.attributes) { - errors.missingAttributes.push( - dictionary.validation.missingAttributesInRowWithFilePath(filePath, index + 1) - ); - } } - return { isValid: errors.general.length === 0, errors }; + return { isValid: errors.length === 0, errors }; }; static validateLocalFile(filePath: string): FileValidationResult { @@ -102,10 +93,7 @@ export class Hip412Validator { } catch (error) { return { isValid: false, - errors: { - general: [errorToMessage(error)], - missingAttributes: [], - }, + errors: [errorToMessage(error)], }; } } @@ -128,7 +116,6 @@ export class Hip412Validator { errors: [ { general: [dictionary.validation.directoryIsEmpty], - missingAttributes: [], }, ], }; @@ -144,8 +131,7 @@ export class Hip412Validator { allFilesValid = false; errors.push({ fileName: file, - general: validationResult.errors.general, - missingAttributes: validationResult.errors.missingAttributes, + general: validationResult.errors, }); } } diff --git a/src/utils/validationError.ts b/src/utils/validationError.ts new file mode 100644 index 0000000..e37c227 --- /dev/null +++ b/src/utils/validationError.ts @@ -0,0 +1,9 @@ +export class ValidationError extends Error { + errors: string[]; + + constructor(errors: string[]) { + super(errors.join(' ')); + this.errors = errors; + this.name = 'ValidationError'; + } +} diff --git a/test/integration/createJsonMetadataFromCsv.test.ts b/test/integration/createJsonMetadataFromCsv.test.ts index d3068ca..0fbffd8 100644 --- a/test/integration/createJsonMetadataFromCsv.test.ts +++ b/test/integration/createJsonMetadataFromCsv.test.ts @@ -30,7 +30,7 @@ describe('createJsonMetadataFromCSV Integration Test', () => { savedJsonFilesLocation: JSON_METADATA_INTEGRATION_TESTS_OUTPUT_FOLDER_PATH, csvFilePath: CSV_EXAMPLE_WITH_ALL_FIELDS, }); - expect(result.errors.general).toHaveLength(0); + expect(result.errors).toHaveLength(0); }); it( @@ -87,7 +87,7 @@ describe('createJsonMetadataFromCSV Integration Test', () => { csvFilePath: CSV_EXAMPLE_ONLY_REQUIRED_FIELDS, }); - expect(result.errors.general).toHaveLength(0); + expect(result.errors).toHaveLength(0); }); it('createJsonMetadataFromCSV should complete without errors using CSV with only required fields and headers filled', async () => { @@ -96,7 +96,7 @@ describe('createJsonMetadataFromCSV Integration Test', () => { csvFilePath: CSV_EXAMPLE_ONLY_REQUIRED_FIELDS_AND_HEADERS, }); - expect(result.errors.general).toHaveLength(0); + expect(result.errors).toHaveLength(0); }); it('createJsonMetadataFromCSV should return errors for missing required fields in CSV', async () => { @@ -104,6 +104,6 @@ describe('createJsonMetadataFromCSV Integration Test', () => { savedJsonFilesLocation: JSON_METADATA_INTEGRATION_TESTS_OUTPUT_FOLDER_PATH, csvFilePath: CSV_EXAMPLE_WITH_MISSING_REQUIRED_FIELDS, }); - expect(result.errors.general).toHaveLength(8); + expect(result.errors).toHaveLength(8); }); }); diff --git a/test/integration/validateLocalFile.test.ts b/test/integration/validateLocalFile.test.ts index f1d56c1..4388ac5 100644 --- a/test/integration/validateLocalFile.test.ts +++ b/test/integration/validateLocalFile.test.ts @@ -6,14 +6,12 @@ describe('Hip412Validator.validateLocalFile integration tests', () => { it('should return an error if the file is empty', () => { const validationResult = Hip412Validator.validateLocalFile(EMPTY_JSON_EXAMPLE_PATH); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toContain( - dictionary.validation.fileEmptyOrFormattingError - ); + expect(validationResult.errors).toContain(dictionary.validation.fileEmptyOrFormattingError); }); it('should validate correctly structured JSON file', () => { const validationResult = Hip412Validator.validateLocalFile(CORRECT_EXAMPLE_PATH); expect(validationResult.isValid).toBe(true); - expect(validationResult.errors.general.length).toBe(0); + expect(validationResult.errors.length).toBe(0); }); }); diff --git a/test/unit/validateLocalFile.test.ts b/test/unit/validateLocalFile.test.ts index 1fad157..97bf0a3 100644 --- a/test/unit/validateLocalFile.test.ts +++ b/test/unit/validateLocalFile.test.ts @@ -16,7 +16,7 @@ describe('Hip412Validator.validateLocalFile', () => { }); const validationResult = Hip412Validator.validateLocalFile('mockPath.json'); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toContain(dictionary.validation.filePermissionDenied); + expect(validationResult.errors).toContain(dictionary.validation.filePermissionDenied); }); it('should handle JSON files with formatting errors', () => { @@ -25,18 +25,14 @@ describe('Hip412Validator.validateLocalFile', () => { }); const validationResult = Hip412Validator.validateLocalFile('mockPath.json'); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toEqual([ - dictionary.validation.fileEmptyOrFormattingError, - ]); + expect(validationResult.errors).toEqual([dictionary.validation.fileEmptyOrFormattingError]); }); it('should handle empty or non-existent JSON files', () => { mockReadFileSync.mockReturnValue(''); const validationResult = Hip412Validator.validateLocalFile('path/to/empty.json'); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toEqual([ - dictionary.validation.fileEmptyOrFormattingError, - ]); + expect(validationResult.errors).toEqual([dictionary.validation.fileEmptyOrFormattingError]); }); it('should validate correctly structured JSON file', () => { @@ -48,6 +44,6 @@ describe('Hip412Validator.validateLocalFile', () => { mockReadFileSync.mockReturnValue(validJson); const validationResult = Hip412Validator.validateLocalFile('path/to/valid.json'); expect(validationResult.isValid).toBe(true); - expect(validationResult.errors.general).toHaveLength(0); + expect(validationResult.errors).toHaveLength(0); }); }); diff --git a/test/unit/validateSingleMetadataObject.test.ts b/test/unit/validateSingleMetadataObject.test.ts index 0df2c0b..03d9783 100644 --- a/test/unit/validateSingleMetadataObject.test.ts +++ b/test/unit/validateSingleMetadataObject.test.ts @@ -8,7 +8,12 @@ const METADATA_OBJECT_WITH_ONLY_REQUIRED_FIELDS = { type: 'image/jpeg', }; -const METADATA_OBJECT_WITH_REQUIRED_FIELDS_MISSING = { +const METADATA_OBJECT_WITH_ONE_REQUIRED_FIELD_MISSING = { + name: 'Example NFT 1', + type: 'image/jpeg', +}; + +const METADATA_OBJECT_WITH_TWO_REQUIRED_FIELDS_MISSING = { name: 'Example NFT 1', }; @@ -56,34 +61,36 @@ describe('Hip412Validator.validateSingleObject', () => { it('should not return any errors for an object with all fields filled properly', () => { const validationResult = validate(METADATA_OBECT_WITH_ALL_FIELDS); expect(validationResult.isValid).toBe(true); - expect(validationResult.errors.general).toHaveLength(0); + expect(validationResult.errors).toHaveLength(0); }); it('should not return any errors for an object with only required fields filled', () => { const validationResult = validate(METADATA_OBJECT_WITH_ONLY_REQUIRED_FIELDS); expect(validationResult.isValid).toBe(true); - expect(validationResult.errors.general).toHaveLength(0); + expect(validationResult.errors).toHaveLength(0); + }); + + it('should return one error for an object missing the image field', () => { + const validationResult = validate(METADATA_OBJECT_WITH_ONE_REQUIRED_FIELD_MISSING); + expect(validationResult.isValid).toBe(false); + expect(validationResult.errors).toHaveLength(1); }); - it('should return an error in errors.general for an object missing the image field', () => { - const validationResult = validate(METADATA_OBJECT_WITH_REQUIRED_FIELDS_MISSING); + it('should return two errors for an object missing the image and type field', () => { + const validationResult = validate(METADATA_OBJECT_WITH_TWO_REQUIRED_FIELDS_MISSING); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toHaveLength(1); + expect(validationResult.errors).toHaveLength(2); }); it('should return an error for an object with an invalid image MIME type', () => { const validationResult = validate(METADATA_OBJECT_WITH_INVALID_IMAGE_TYPE); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toEqual([ - dictionary.validation.requiredTypeFieldIsMissing, - ]); + expect(validationResult.errors).toEqual([dictionary.validation.requiredTypeFieldIsMissing]); }); it('should return an error for an object with an invalid attributes structure', () => { const validationResult = validate(METADATA_OBJECT_WITH_INVALID_ATTRIBUTES_STRUCTURE); expect(validationResult.isValid).toBe(false); - expect(validationResult.errors.general).toEqual([ - dictionary.validation.requiredAttributeFieldMissing, - ]); + expect(validationResult.errors).toEqual([dictionary.validation.requiredAttributeFieldMissing]); }); });