Skip to content

Commit

Permalink
#195: Add keyfield and keys to schema validation error report (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohit-s96 authored Nov 9, 2024
1 parent a400635 commit bfa9a2d
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 21 deletions.
169 changes: 155 additions & 14 deletions lib/schema/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const { validationContext } = require('./context');

const IGNORE_ENUMS = 'ignoreEnumerations';

const isReservedKey = key => {
const reservedKeys = ['__proto__', 'constructor', 'prototype'];
return reservedKeys.includes(key);
};

/**
* @param {Object} obj
* @param {string} obj.inputPath
Expand Down Expand Up @@ -131,7 +136,7 @@ const writeFile = async (path, data) => {
* @param {string[]} obj.arr
* @param {Object} obj.metadataMap
* @param {string} obj.parentResourceName
* @returns {{parentResourceName: string, sourceModel: string?, sourceModelField: string?, fieldName: string?}}
* @returns {{parentResourceName: string, sourceModel: string?, sourceModelField: string?, fieldName: string?, index: number?, expansionIndex: number?}}
*/
const parseNestedPropertyForResourceAndField = ({ arr, metadataMap, parentResourceName }) => {
return arr.reduce(
Expand Down Expand Up @@ -160,11 +165,24 @@ const parseNestedPropertyForResourceAndField = ({ arr, metadataMap, parentResour
fieldName: field
};
}
} else if (!isNaN(Number(field))) {
if (acc.index === null) {
return {
...acc,
index: Number(field)
};
} else if (acc.expansionIndex === null) {
return {
...acc,
expansionIndex: Number(field)
};
}
return acc;
} else {
return acc;
}
},
{ parentResourceName, sourceModel: null, sourceModelField: null, fieldName: null }
{ parentResourceName, sourceModel: null, sourceModelField: null, fieldName: null, index: null, expansionIndex: null }
);
};

Expand Down Expand Up @@ -249,6 +267,7 @@ const addCustomValidationForEnum = ajv => {
const errorMessage = {
keyword: 'enum',
failedItemValue: enumValue,
failedEnum: enumValue,
params: {
allowedValues: schema
},
Expand Down Expand Up @@ -385,15 +404,42 @@ const combineErrors = ({ errorCache, warningsCache, payloadErrors, stats, versio
const occurrences = Object.entries(files).flatMap(([fileName, values]) => {
const file = path.basename(fileName || '');
const pathName = fileName;
return Object.entries(values).map(([lookupValue, details]) => ({
count: details.occurrences,
...(file && { fileName: file }),
...(pathName && { pathName }),
...(lookupValue && { lookupValue }),
...(details?.sourceModel && { sourceModel: details.sourceModel }),
...(details?.sourceModelField && { sourceModelField: details.sourceModelField }),
message: details.message
}));
return Object.entries(values).map(([lookupValue, details]) => {
const result = {
count: details.occurrences,
message: details.message
};

if (file) {
result.fileName = file;
}

if (pathName) {
result.pathName = pathName;
}

if (details?.failedEnum != null) {
result.lookupValue = lookupValue;
}

if (details?.sourceModel) {
result.sourceModel = details.sourceModel;
}

if (details?.sourceModelField) {
result.sourceModelField = details.sourceModelField;
}

if (details?.keyField) {
result.keyField = details.keyField;
}

if (details?.keys) {
result.keys = details.keys;
}

return result;
});
});

const message = occurrences?.[0]?.message ?? '';
Expand Down Expand Up @@ -457,6 +503,9 @@ const combineErrors = ({ errorCache, warningsCache, payloadErrors, stats, versio
* @param {String} obj.fileName
* @param {String} obj.sourceModel
* @param {String} obj.sourceModelField
* @param {String | null} obj.keyField
* @param {String | null} obj.keyFieldValue
* @param {String?} obj.failedEnum
* @param {{totalErrors: Number, totalWarnings: Number}} obj.stats
* @param {boolean} obj.isWarning
*
Expand All @@ -472,9 +521,16 @@ const updateCacheAndStats = ({
failedItemValue,
isWarning,
sourceModel,
sourceModelField
sourceModelField,
keyField,
keyFieldValue,
failedEnum
}) => {
const filePath = fileName;
if (isReservedKey(filePath)) {
return;
}

if (!cache[resourceName]) {
cache[resourceName] = {};
}
Expand All @@ -492,7 +548,7 @@ const updateCacheAndStats = ({
}

if (!cache[resourceName][failedItemName][message][filePath][failedItemValue]) {
cache[resourceName][failedItemName][message][filePath][failedItemValue] = {
const error = {
// Capitalize first word like MUST, SHOULD, etc.
message: !message.startsWith(isWarning ? 'The' : 'Fields')
? message.slice(0, message.indexOf(' ')).toUpperCase() + message.slice(message.indexOf(' '), message.length)
Expand All @@ -501,8 +557,21 @@ const updateCacheAndStats = ({
sourceModel,
sourceModelField
};
if (keyField && keyFieldValue) {
error.keyField = keyField;
}
if (keyFieldValue) {
error.keys = [keyFieldValue];
}
if (failedEnum !== null && failedEnum !== undefined) {
error.failedEnum = failedEnum;
}
cache[resourceName][failedItemName][message][filePath][failedItemValue] = error;
} else {
cache[resourceName][failedItemName][message][filePath][failedItemValue].occurrences++;
if (cache[resourceName]?.[failedItemName]?.[message]?.[filePath]?.[failedItemValue]?.keys && keyFieldValue) {
cache[resourceName]?.[failedItemName]?.[message]?.[filePath]?.[failedItemValue]?.keys.push(keyFieldValue);
}
}

if (isWarning) {
Expand Down Expand Up @@ -550,6 +619,75 @@ const getMaxLengthMessage = (limitObject, isRCF) => {
}
};

const keyifyResourceName = resourceName => {
return resourceName.trim() + 'Key';
};

const getKeyFieldForResource = resourceName => {
switch (resourceName) {
case 'Property':
return 'ListingKey';
case 'Contacts':
case 'ContactListingNotes':
return 'ContactKey';
case 'InternetTracking':
return 'EventKey';
case 'InternetTrackingSummary':
return 'ListingId';
case 'OUID':
return 'OrganizationUniqueIdKey';
case 'Queue':
return 'QueueTransactionKey';
case 'PropertyGreenVerification':
return 'GreenBuildingVerificationKey';
case 'PropertyRooms':
return 'RoomKey';
case 'PropertyUnitTypes':
return 'UnitTypeKey';
case 'EntityEvent':
return 'EntityEventSequence';
case 'MemberAssociation':
case 'OfficeAssociation':
return 'AssociationKey';
case 'TransactionManagement':
return 'TransactionKey';
case 'Rules':
return 'RuleKey';
case 'Teams':
return 'TeamKey';
case 'TeamMembers':
return 'TeamMemberKey';
case 'PropertyPowerProduction':
return 'PowerProductionKey';
case 'PropertyPowerStorage':
return 'PowerStorageKey';
default:
return keyifyResourceName(resourceName);
}
};

const getValueForKeyField = ({ keyField, payload, index, expansionIndex, expansionField, isExpansion }) => {
if (isExpansion) {
if (index != null) {
let keyValue;
if (expansionIndex != null) {
keyValue = payload?.value?.[index]?.[expansionField]?.[expansionIndex]?.[keyField];
} else {
keyValue = payload?.value?.[index]?.[expansionField]?.[keyField];
}
if (keyValue) return keyValue;
}
} else {
if (index != null) {
const keyValue = payload?.value?.[index]?.[keyField];
if (keyValue) {
return keyValue;
}
}
}
return null;
};

module.exports = {
processFiles,
readFile,
Expand All @@ -567,5 +705,8 @@ module.exports = {
combineErrors,
updateCacheAndStats,
getResourceAndVersion,
getMaxLengthMessage
getMaxLengthMessage,
getKeyFieldForResource,
getValueForKeyField,
isReservedKey
};
28 changes: 23 additions & 5 deletions lib/schema/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const {
getResourceAndVersion,
getMaxLengthMessage,
VALIDATION_ERROR_MESSAGES,
SCHEMA_ERROR_KEYWORDS
SCHEMA_ERROR_KEYWORDS,
getKeyFieldForResource,
getValueForKeyField
} = require('./utils');
const { validationContext } = require('./context');

Expand Down Expand Up @@ -270,7 +272,7 @@ const generateErrorReport = ({
isRCF,
metadataMap
}) => {
validate.errors.reduce((acc, { instancePath, message, keyword, params, failedItemValue: value, isWarning, transformedValue }) => {
validate.errors.reduce((acc, { instancePath, message, keyword, params, failedItemValue: value, isWarning, transformedValue, failedEnum }) => {
if (!instancePath && keyword !== SCHEMA_ERROR_KEYWORDS.ADDITIONAL_PROPERTIES) return acc;
const nestedPayloadProperties = instancePath?.split('/')?.slice(1) || [];
let failedItemValue = '';
Expand All @@ -295,12 +297,12 @@ const generateErrorReport = ({
failedItemValue = '';
}

// eslint-disable-next-line prefer-const
let { fieldName, sourceModel, sourceModelField } = parseNestedPropertyForResourceAndField({
const { fieldName, sourceModel, sourceModelField: modelField, index, expansionIndex } = parseNestedPropertyForResourceAndField({
arr: nestedPayloadProperties,
metadataMap: schema?.definitions?.MetadataMap,
parentResourceName: resourceName
});
let sourceModelField = modelField;

if (
keyword === SCHEMA_ERROR_KEYWORDS.TYPE &&
Expand Down Expand Up @@ -350,6 +352,7 @@ const generateErrorReport = ({
}

if (resolvedKeyword === SCHEMA_ERROR_KEYWORDS.MAX_LENGTH) {
failedItemValue = '';
if (isRCF) {
isWarning = true;
} else {
Expand All @@ -360,6 +363,18 @@ const generateErrorReport = ({
}
}

const multiPayload = validationContext.getPayloadType() === 'MULTI';
const payload = multiPayload ? json : { value: [json] };
const keyField = isWarning ? null : getKeyFieldForResource(sourceModel ?? resourceName);
const keyFieldValue = isWarning ? null : getValueForKeyField({
keyField,
payload,
index: multiPayload ? index : 0,
expansionIndex: multiPayload ? expansionIndex : index,
expansionField: failedItemName,
isExpansion: !!sourceModel
});

updateCacheAndStats({
cache: isWarning ? warningsCache : errorCache,
resourceName: resourceName,
Expand All @@ -370,7 +385,10 @@ const generateErrorReport = ({
failedItemValue,
isWarning,
sourceModel,
sourceModelField
sourceModelField,
keyField,
keyFieldValue,
failedEnum
});
return acc;
}, {});
Expand Down
47 changes: 46 additions & 1 deletion test/schema/payload-samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,50 @@ const topLevelUnadvertisedField = {
Foo: false
};

const keyFieldPayloadMulti = {
'@reso.context': 'urn:reso:metadata:1.7:resource:property',
value: [
{
Country: 'CA',
StateOrProvince: 'ON',
City: 'NYC',
ListingKey: 'listingkey1',
Media: [
{
ResourceName: 'Property',
MediaCategory: 'Branded Virtual Tour',
MediaURL: 'https://example.com/vJVDL415WZ7GE1/',
ShortDescription: 'Example',
MediaKey: 'mediakey1'
},
{
ResourceName: 'Property',
MediaCategory: 'Branded Virtual Tour',
MediaURL: 'https://example.com/vJVDL415WZ7GE1/doc/floorplan_imperial.pdf',
ShortDescription: 'imperial',
MediaKey: 'mediakey2'
}
],
Rooms: [
{
RoomWidth: 4.409,
RoomLength: 2.977,
RoomLengthWidthUnits: 'Meters',
RoomKey: 'roomkey1',
RoomLengthWidthSource: 'LocalProvider'
},
{
RoomWidth: 4.3,
RoomLength: 5.998,
RoomLengthWidthUnits: 'Meters',
RoomKey: 'roomkey2',
RoomLengthWidthSource: 'LocalProvider'
}
]
}
]
};

module.exports = {
valuePayload,
nonValuePayload,
Expand Down Expand Up @@ -500,5 +544,6 @@ module.exports = {
expansionIgnoredItem,
collectionExpansionError,
singleValueExpansionError,
topLevelUnadvertisedField
topLevelUnadvertisedField,
keyFieldPayloadMulti
};
Loading

0 comments on commit bfa9a2d

Please sign in to comment.