@common-grants/cli
Version:
The CommonGrants protocol CLI tool
266 lines (265 loc) • 10.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkSchemaCompatibility = checkSchemaCompatibility;
const flatten_schemas_1 = require("./flatten-schemas");
const error_utils_1 = require("./error-utils");
const ERROR_TYPE = "ROUTE_CONFLICT";
// ############################################################
// Main function
// ############################################################
/**
* Checks whether `implSchema` is a valid subset of `baseSchema`.
*
* We treat `baseSchema` as more "generic". That means:
* 1) If baseSchema.type is undefined, that is "any" => allow impl any type
* 2) If baseSchema has `additionalProperties` as a schema, any extra fields in impl
* must conform to that schema
* 3) Required fields in baseSchema must be present in impl
* 4) If baseSchema has a typed property (e.g. 'string', 'object'), then
* impl must match that type (unless base has no type).
* 5) If baseSchema has an enum, impl must include all base enum values
*/
function checkSchemaCompatibility(location, baseSchema, implSchema, ctx) {
let errors = new error_utils_1.ErrorCollection();
// 1) Skip if base has no type
if (!baseSchema.type) {
return errors;
}
// 2) Check type conflict
errors = checkTypeConflict(location, baseSchema, implSchema, ctx, errors);
// 3) If the schema is object-typed, compare properties
if (baseSchema.type === "object") {
checkObjectCompatibility(location, baseSchema, implSchema, ctx, errors);
}
// 4) If the schema is array-typed, compare items
if (baseSchema.type === "array") {
checkArrayCompatibility(location, baseSchema, implSchema, ctx, errors);
}
// 5) If the schema has an enum, verify that impl doesn't have extra values
errors = checkEnumConflict(location, baseSchema, implSchema, ctx, errors);
return errors;
}
// ############################################################
// Helper functions - Simple types
// ############################################################
/**
* Checks whether `implSchema` has a different type than `baseSchema`.
*/
function checkTypeConflict(location, baseSchema, implSchema, ctx, errors) {
if (baseSchema.type && implSchema.type && baseSchema.type !== implSchema.type) {
const error = typeConflictError(location, baseSchema.type, implSchema.type, ctx);
errors.addError(error);
}
return errors;
}
/**
* Checks whether `implSchema` has any enum values that are not in `baseSchema`.
*/
function checkEnumConflict(location, baseSchema, implSchema, ctx, errors) {
if (Array.isArray(baseSchema.enum) && Array.isArray(implSchema.enum)) {
for (const implVal of implSchema.enum) {
if (!baseSchema.enum.includes(implVal)) {
const error = enumConflictError(location, implVal, ctx);
errors.addError(error);
}
}
}
return errors;
}
// ############################################################
// Helper functions - Object types
// ############################################################
/**
* Checks whether object properties are compatible.
*
* This is a new implementation of the checkObjectCompatibility function.
* It is more efficient and easier to understand.
*/
function checkObjectCompatibility(location, baseSchema, implSchema, ctx, errors) {
// Step 1: Get matching, missing, and extra properties
const baseProps = baseSchema.properties || {};
const implProps = implSchema.properties || {};
const propsByStatus = getPropsByStatus(baseProps, implProps);
// Step 2: Check that matching props are compatible
for (const propName of propsByStatus.matching) {
const baseProp = baseProps[propName];
const implProp = implProps[propName];
const propLoc = `${location}.${propName}`;
const propErrors = checkSchemaCompatibility(propLoc, baseProp, implProp, ctx);
errors.addErrors(propErrors.getAllErrors());
}
// Step 3: Handle missing props
for (const propName of propsByStatus.missing) {
const propLoc = `${location}.${propName}`;
const error = missingFieldError(propLoc, propName, { ...ctx, baseSchema });
errors.addError(error);
}
// Step 4: Handle extra props
//TODO: @widal001 (2025-06-06) - Make this more robust
// WHEN additionalProperties or unevaluatedProperties is:
// 1. undefined || true => allow any extra props
// 2. false || not: {} => disallow any extra props
// 3. schema => validate extra props against that schema
const extraPropsAllowed = baseSchema.additionalProperties === true;
const extraPropsSchema = baseSchema.additionalProperties;
if (extraPropsAllowed) {
// If additionalProperties are allowed, skip
return errors;
}
else if (extraPropsSchema) {
// If additionalProperties need to match a schema,
// validate extra props against that schema
const flattenedExtraPropsSchema = (0, flatten_schemas_1.deepFlattenAllOf)(extraPropsSchema);
for (const propName of propsByStatus.extra) {
const implProp = implProps[propName];
const propLoc = `${location}.${propName}`;
const propErrors = checkSchemaCompatibility(propLoc, flattenedExtraPropsSchema, implProp, ctx);
errors.addErrors(propErrors.getAllErrors());
}
}
else {
// Otherwise, extra props are not allowed, and should be flagged as errors
for (const propName of propsByStatus.extra) {
const propLoc = `${location}.${propName}`;
const error = extraFieldError(propLoc, propName, ctx);
errors.addError(error);
}
}
return errors;
}
/**
* Separates properties into matching, missing, and extra.
*
* - matching: properties that are present in both base and impl
* - missing: properties that are present in base but not in impl
* - extra: properties that are present in impl but not in base
*/
function getPropsByStatus(baseProps, implProps) {
const matching = [];
const missing = [];
const extra = [];
// Check all base properties
for (const propName of Object.keys(baseProps)) {
if (propName in implProps) {
matching.push(propName);
}
else {
missing.push(propName);
}
}
// Check for extra properties in implementation
for (const propName of Object.keys(implProps)) {
if (!(propName in baseProps)) {
extra.push(propName);
}
}
return { matching, missing, extra };
}
// ############################################################
// Helper functions - Array types
// ############################################################
/**
* Checks whether array items are compatible.
*
* This function validates that:
* 1. The implementation schema has items defined
* 2. The items in the implementation schema are compatible with the base schema's items
*/
function checkArrayCompatibility(location, baseSchema, implSchema, ctx, errors) {
// If base schema has no items defined, any items are allowed
if (!baseSchema.items) {
return errors;
}
// If impl schema has no items defined, that's an error
if (!implSchema.items) {
const error = {
type: ERROR_TYPE,
level: "ERROR",
subType: ctx.errorSubType,
endpoint: ctx.endpoint,
statusCode: ctx.statusCode,
mimeType: ctx.mimeType,
conflictType: "MISSING_FIELD",
message: `Array schema must define items`,
location,
};
errors.addError(error);
return errors;
}
// Check that the items are compatible
const baseItems = baseSchema.items;
const implItems = implSchema.items;
const itemErrors = checkSchemaCompatibility(`${location}[0]`, baseItems, implItems, ctx);
errors.addErrors(itemErrors.getAllErrors());
return errors;
}
// ############################################################
// Error creation functions
// ############################################################
/**
* Creates an error when types are mismatched.
*/
function typeConflictError(location, baseType, implType, ctx) {
return {
type: ERROR_TYPE,
level: "ERROR",
subType: ctx.errorSubType,
endpoint: ctx.endpoint,
statusCode: ctx.statusCode,
mimeType: ctx.mimeType,
conflictType: "TYPE_CONFLICT",
message: `Type mismatch. Base is '${baseType}', impl is '${implType}'`,
baseType,
implType,
location,
};
}
/**
* Creates an error when implementation has extra enum values.
*/
function enumConflictError(location, extraValue, ctx) {
return {
type: ERROR_TYPE,
level: "ERROR",
subType: ctx.errorSubType,
endpoint: ctx.endpoint,
statusCode: ctx.statusCode,
mimeType: ctx.mimeType,
conflictType: "ENUM_CONFLICT",
message: `Enum mismatch. Extra value '${extraValue}' in implementation not allowed by base spec`,
location,
};
}
/**
* Creates an error when implementation is missing a required field.
*/
function missingFieldError(location, fieldName, ctx) {
const isRequired = ctx.baseSchema?.required?.includes(fieldName);
return {
type: ERROR_TYPE,
level: "ERROR",
subType: ctx.errorSubType,
endpoint: ctx.endpoint,
statusCode: ctx.statusCode,
mimeType: ctx.mimeType,
conflictType: "MISSING_FIELD",
message: `Missing ${isRequired ? "required" : "optional"} property '${fieldName}'`,
location,
};
}
/**
* Creates an error when implementation has extra properties.
*/
function extraFieldError(location, fieldName, ctx) {
return {
type: ERROR_TYPE,
level: "ERROR",
subType: ctx.errorSubType,
endpoint: ctx.endpoint,
statusCode: ctx.statusCode,
mimeType: ctx.mimeType,
conflictType: "EXTRA_FIELD",
message: `Implementation schema has extra property '${fieldName}' not defined in base schema (and 'additionalProperties' is not allowed)`,
location,
};
}