From 1dc2f4e52254ff14a3cfd28ab9f0db414cbc61a4 Mon Sep 17 00:00:00 2001 From: Patrick Cuba Date: Mon, 15 Jun 2026 11:13:45 -0500 Subject: [PATCH 1/3] fixes #530 better handling during import of manifests if language maps have non-`none` objects. --- test/local/utility.validation.test.js | 64 +++++++++++++++++++++++ utilities/validatePayload.js | 73 ++++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/test/local/utility.validation.test.js b/test/local/utility.validation.test.js index aa19890d..a11119d1 100644 --- a/test/local/utility.validation.test.js +++ b/test/local/utility.validation.test.js @@ -60,4 +60,68 @@ describe('Validation utilities', () => { const filteredObject = scrubDefaultRoles({ OWNER: ['*'], CUSTOM_ROLE: ['READ_*_*'] }) assert.deepEqual(filteredObject, { CUSTOM_ROLE: ['READ_*_*'] }, 'OWNER key must be stripped from a roles map') }) + + it('accepts metadata with non-"none" language tags', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: { en: ['Author'] }, value: { en: ['John Doe'] } }, + { label: { fr: ['Auteur'] }, value: { fr: ['Jean Dupont'] } }, + { label: { la: ['Auctor'] }, value: { la: ['Iohannes'] } } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, true) + assert.equal(result.errors, null) + }) + + it('accepts metadata with mixed language tags and plain strings', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: 'Source', value: 'Test Source' }, + { label: { en: ['Title'] }, value: 'Plain String Value' }, + { label: 'Date', value: { en: ['2024-01-01'] } } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, true) + assert.equal(result.errors, null) + }) + + it('rejects metadata with empty language map', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: {}, value: 'test value' } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, false) + assert.match(result.errors ?? '', /language map/) + }) + + it('rejects metadata with language map containing non-string values', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: { en: ['Author'] }, value: { en: [123] } } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, false) + assert.match(result.errors ?? '', /language map/) + }) + + it('rejects metadata with language map containing empty array', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: { en: ['Author'] }, value: { en: [] } } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, false) + assert.match(result.errors ?? '', /language map/) + }) }) diff --git a/utilities/validatePayload.js b/utilities/validatePayload.js index 090579d1..d17f54cf 100644 --- a/utilities/validatePayload.js +++ b/utilities/validatePayload.js @@ -1,3 +1,63 @@ +/** + * Validates that a language map object (per IIIF spec) has at least one language key + * with an array of string values. + * + * @param {Object} langMap - Object keyed by IETF language tag (e.g., { "en": ["value"], "fr": ["valeur"] }). + * @returns {boolean} - True if the language map is valid. + */ +function validateLanguageMap(langMap) { + if (typeof langMap !== 'object' || langMap === null) + return false + + const keys = Object.keys(langMap) + if (keys.length === 0) + return false + + return keys.some(key => { + const values = langMap[key] + return Array.isArray(values) && values.length > 0 && values.every(v => typeof v === 'string') + }) +} + +/** + * Validates a metadata label or value field that can be a plain string or a language map. + * + * @param {string} fieldName - The field name for error messages ("label" or "value"). + * @param {*} fieldValue - The value to validate. + * @param {boolean} allowEmpty - Whether empty strings are permitted (true for values, false for labels). + * @returns {Object|null} - Returns { isValid: false, errors: string } on failure, or null on success. + */ +function validateMetadataField(fieldName, fieldValue, allowEmpty) { + const isString = typeof fieldValue === 'string' + const isLangMap = typeof fieldValue === 'object' && fieldValue !== null + + // Invalid type entirely + if (!isString && !isLangMap) { + const message = fieldName === 'label' + ? `metadata item label must be a string or language map object` + : 'metadata item value cannot be processed' + return { isValid: false, errors: message } + } + + // Plain string + if (isString) { + if (!allowEmpty && fieldValue.trim() === '') { + return { isValid: false, errors: `metadata item ${fieldName} must be a non-empty string` } + } + return null + } + + // Language map + if (!validateLanguageMap(fieldValue)) { + const message = allowEmpty + ? `metadata item ${fieldName} must be a valid language map with string values` + : `metadata item ${fieldName} must be a valid language map with non-empty string values` + return { isValid: false, errors: message } + } + + return null +} + /** * Validate that the provided data payload is a valid Project object. * This function validates both the presence and the data types of required project properties. @@ -49,15 +109,12 @@ export function validateProjectPayload(payload) { return { isValid: false, errors: 'metadata item must have label and value properties' } } - // Validate label and value are non-empty strings - if (typeof metadataItem.label?.none?.[0] === 'string' && metadataItem.label?.none?.[0].trim() === '') - return { isValid: false, errors: 'metadata item label must be a non-empty language map' } - - if (typeof metadataItem.label === 'string' && metadataItem.label.trim() === '') - return { isValid: false, errors: 'metadata item label must be a non-empty string' } + // Validate label and value - accept plain string or language-mapped object (any IETF language tag) + const labelError = validateMetadataField('label', metadataItem.label, false) + if (labelError) return labelError - if (typeof metadataItem.value?.none?.[0] !== 'string' && typeof metadataItem.value !== 'string') - return { isValid: false, errors: 'metadata item value cannot be processed' } + const valueError = validateMetadataField('value', metadataItem.value, true) + if (valueError) return valueError // Ensure no extra properties beyond label and value const allowedProps = ['label', 'value'] From 3b9c4022e44d24eac1dbf78d3c0b61b1090116bb Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 6 Jul 2026 14:00:03 -0500 Subject: [PATCH 2/3] Changes during testing --- test/local/utility.validation.test.js | 24 ++++++++++++++++++++++++ utilities/validatePayload.js | 15 +++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/test/local/utility.validation.test.js b/test/local/utility.validation.test.js index a11119d1..3c880c95 100644 --- a/test/local/utility.validation.test.js +++ b/test/local/utility.validation.test.js @@ -124,4 +124,28 @@ describe('Validation utilities', () => { assert.equal(result.isValid, false) assert.match(result.errors ?? '', /language map/) }) + + it('rejects a language map label whose values are only empty strings', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: { en: [''] }, value: 'test value' } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, false) + assert.match(result.errors ?? '', /language map/) + }) + + it('rejects a language map where one of several keys is malformed', () => { + const payload = buildValidProjectPayload() + payload.metadata = [ + { label: { en: ['Author'], fr: [123] }, value: 'test value' } + ] + + const result = validateProjectPayload(payload) + + assert.equal(result.isValid, false) + assert.match(result.errors ?? '', /language map/) + }) }) diff --git a/utilities/validatePayload.js b/utilities/validatePayload.js index d17f54cf..4da201cf 100644 --- a/utilities/validatePayload.js +++ b/utilities/validatePayload.js @@ -1,11 +1,12 @@ /** * Validates that a language map object (per IIIF spec) has at least one language key - * with an array of string values. - * + * and that every language key maps to a non-empty array of strings. + * * @param {Object} langMap - Object keyed by IETF language tag (e.g., { "en": ["value"], "fr": ["valeur"] }). + * @param {boolean} [allowEmpty=true] - Whether empty or whitespace-only string values are permitted. * @returns {boolean} - True if the language map is valid. */ -function validateLanguageMap(langMap) { +function validateLanguageMap(langMap, allowEmpty = true) { if (typeof langMap !== 'object' || langMap === null) return false @@ -13,9 +14,11 @@ function validateLanguageMap(langMap) { if (keys.length === 0) return false - return keys.some(key => { + return keys.every(key => { const values = langMap[key] - return Array.isArray(values) && values.length > 0 && values.every(v => typeof v === 'string') + return Array.isArray(values) + && values.length > 0 + && values.every(v => typeof v === 'string' && (allowEmpty || v.trim() !== '')) }) } @@ -48,7 +51,7 @@ function validateMetadataField(fieldName, fieldValue, allowEmpty) { } // Language map - if (!validateLanguageMap(fieldValue)) { + if (!validateLanguageMap(fieldValue, allowEmpty)) { const message = allowEmpty ? `metadata item ${fieldName} must be a valid language map with string values` : `metadata item ${fieldName} must be a valid language map with non-empty string values` From f8bf71717424b85730dca02cd650e5511c79367b Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Mon, 6 Jul 2026 14:08:10 -0500 Subject: [PATCH 3/3] Minor error message wording change --- utilities/validatePayload.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/utilities/validatePayload.js b/utilities/validatePayload.js index 4da201cf..913714cc 100644 --- a/utilities/validatePayload.js +++ b/utilities/validatePayload.js @@ -36,10 +36,7 @@ function validateMetadataField(fieldName, fieldValue, allowEmpty) { // Invalid type entirely if (!isString && !isLangMap) { - const message = fieldName === 'label' - ? `metadata item label must be a string or language map object` - : 'metadata item value cannot be processed' - return { isValid: false, errors: message } + return { isValid: false, errors: `metadata item ${fieldName} must be a string or language map object` } } // Plain string