master-zapier-plan-draft/vendor/zapier-platform/packages/schema/lib/functional-constraints/validateJsonFieldSchema.js
George Lambert b4150c8250 Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe),
packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier
(CLI app), vendor/zapier-platform, and research/zapier vendor corpus.

Gate 0 structure checks pass. Product code and research are not yet wired.
2026-09-09 02:37:36 -04:00

254 lines
8.3 KiB
JavaScript

'use strict';
const _ = require('lodash');
const jsonschema = require('jsonschema');
const JSON_SCHEMA_SCHEMA_ID = '/JsonSchemaSchema';
const PLAIN_INPUT_FIELD_SCHEMA_ID = '/PlainInputFieldSchema';
const actionTypes = ['triggers', 'searches', 'creates', 'bulkReads'];
const resourceMethods = ['get', 'list', 'hook', 'search', 'create'];
// Load supported JSON Schema meta-schemas
const draft4MetaSchema = require('json-metaschema/draft-04-schema.json');
const draft6MetaSchema = require('json-metaschema/draft-06-schema.json');
const draft7MetaSchema = require('json-metaschema/draft-07-schema.json');
// Map of supported $schema URIs (without trailing #) to their meta-schemas
// HTTPS supported but normalized below
const SUPPORTED_META_SCHEMAS = {
'http://json-schema.org/draft-04/schema': draft4MetaSchema,
'http://json-schema.org/draft-06/schema': draft6MetaSchema,
'http://json-schema.org/draft-07/schema': draft7MetaSchema,
};
const metaValidator = new jsonschema.Validator();
// Translates jsonschema ValidationError from meta-schema validation
// into a human-readable error message.
const formatMetaSchemaError = (error, rootPath) => {
const ALLOWED_TYPE_NAMES = [
'object',
'array',
'string',
'number',
'integer',
'boolean',
'null',
];
const relativePath = error.property.replace(/^instance\.?/, '');
const fullPath = relativePath ? `${rootPath}.${relativePath}` : rootPath;
// "type" field: invalid JSON Schema type value
if (/\.type$/.test(error.property) || error.property === 'instance.type') {
if (error.name === 'anyOf') {
const value = error.instance;
if (typeof value === 'string') {
return `${fullPath}: invalid type "${value}". Must be one of: ${ALLOWED_TYPE_NAMES.join(', ')}`;
}
if (Array.isArray(value)) {
const invalid = value.filter(
(t) => typeof t !== 'string' || !ALLOWED_TYPE_NAMES.includes(t),
);
if (invalid.length > 0) {
return invalid
.map(
(t) =>
`${fullPath}: invalid type "${t}". Must be one of: ${ALLOWED_TYPE_NAMES.join(', ')}`,
)
.join('; ');
}
return `${fullPath}: must be a string or array of strings`;
}
return `${fullPath}: must be a string or array of strings`;
}
}
// Non-object where a JSON Schema object is expected
// Draft 7 allows boolean schemas, so argument may be ['object', 'boolean']
if (
error.name === 'type' &&
Array.isArray(error.argument) &&
error.argument.includes('object')
) {
return `${fullPath}: must be a valid JSON Schema object`;
}
// Non-array where an array is expected (required, enum, allOf, etc.)
if (error.name === 'type' && _.isEqual(error.argument, ['array'])) {
const fieldName = fullPath.split('.').pop();
return `${fieldName}: must be an array`;
}
// anyOf failure for fields expecting a schema or specific structure
if (error.name === 'anyOf') {
const value = error.instance;
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return `${fullPath}: must be a valid JSON Schema object`;
}
return `${fullPath}: must be a valid JSON Schema`;
}
// Default fallback
return `${fullPath}: ${error.message}`;
};
// Resolves which meta-schema to validate against based on the $schema field.
// Returns { metaSchema, error } where error is a string if $schema is unsupported.
const resolveMetaSchema = (schema) => {
if (!schema.$schema) {
return { metaSchema: draft7MetaSchema, error: null };
}
if (typeof schema.$schema !== 'string') {
return { metaSchema: null, error: '`$schema` must be a string' };
}
const normalizedUri = schema.$schema
.replace(/#$/, '')
.replace(/^https:/, 'http:');
const metaSchema = SUPPORTED_META_SCHEMAS[normalizedUri];
if (!metaSchema) {
const supported = Object.keys(SUPPORTED_META_SCHEMAS).join(', ');
return {
metaSchema: null,
error: `unsupported JSON Schema version "${schema.$schema}". Supported versions: ${supported}`,
};
}
return { metaSchema, error: null };
};
// Validates that an object is a structurally valid JSON schema
// using the appropriate meta-schema via jsonschema library.
// Returns an array of error message strings
const collectSchemaErrors = (schema, rootPath) => {
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
return [`${rootPath}: must be a valid JSON Schema object`];
}
const { metaSchema, error } = resolveMetaSchema(schema);
if (error) {
return [`${rootPath}: ${error}`];
}
const result = metaValidator.validate(schema, metaSchema);
return result.errors.map((err) => formatMetaSchemaError(err, rootPath));
};
const checkSchemaField = (field, path) => {
let errors = [];
if (_.has(field, 'schema')) {
if (field.type !== 'json') {
errors.push(
new jsonschema.ValidationError(
'must have `type` set to `json` when `schema` is provided.',
field,
'/PlainInputFieldSchema',
path,
'invalidSchema',
'schema',
),
);
} else {
// Root schema type must be object or array, not primitives
const rootType = field.schema.type;
if (rootType !== undefined) {
const types = Array.isArray(rootType) ? rootType : [rootType];
const invalidTypes = types.filter(
(t) => t !== 'object' && t !== 'array',
);
if (invalidTypes.length > 0) {
errors.push(
new jsonschema.ValidationError(
`has an invalid JSON Schema in \`schema\`: schema: root \`type\` must be "object" or "array", got ${invalidTypes.map((t) => `"${t}"`).join(', ')}`,
field,
'/PlainInputFieldSchema',
path,
'invalidJsonSchema',
'schema',
),
);
// Skip meta-schema validation if root type is invalid
return errors;
}
}
const schemaErrors = collectSchemaErrors(field.schema, 'schema');
schemaErrors.forEach((message) => {
errors.push(
new jsonschema.ValidationError(
`has an invalid JSON Schema in \`schema\`: ${message}`,
field,
'/PlainInputFieldSchema',
path,
'invalidJsonSchema',
'schema',
),
);
});
}
}
// Recurse into children (nested PlainInputFieldSchema)
(field.children || []).forEach((child, childIndex) => {
errors = errors.concat(
checkSchemaField(child, `${path}.children[${childIndex}]`),
);
});
return errors;
};
const validateJsonFieldSchema = (definition, mainSchema) => {
let errors = [];
// Handle individual field validation (for auto-tests of examples/antiExamples)
if (mainSchema.id === PLAIN_INPUT_FIELD_SCHEMA_ID) {
return checkSchemaField(definition, 'instance');
} else if (mainSchema.id === JSON_SCHEMA_SCHEMA_ID) {
return checkSchemaField(
// Make a fake PlainInputField object so checkSchemaField detects a json type and `schema` property
{ key: 'placeholder_field_key', type: 'json', schema: definition },
'instance',
);
}
// Handle full app definition validation
_.each(actionTypes, (actionType) => {
if (definition[actionType]) {
_.each(definition[actionType], (actionDef) => {
if (actionDef.operation && actionDef.operation.inputFields) {
_.each(actionDef.operation.inputFields, (field, index) => {
const path = `instance.${actionType}.${actionDef.key}.operation.inputFields[${index}]`;
errors = errors.concat(checkSchemaField(field, path));
});
}
});
}
});
// Handle resources if they exist
if (definition.resources) {
_.each(definition.resources, (resource, resourceKey) => {
resourceMethods.forEach((method) => {
if (
resource[method] &&
resource[method].operation &&
resource[method].operation.inputFields
) {
_.each(resource[method].operation.inputFields, (field, index) => {
const path = `instance.resources.${resourceKey}.${method}.operation.inputFields[${index}]`;
errors = errors.concat(checkSchemaField(field, path));
});
}
});
});
}
return errors;
};
module.exports = validateJsonFieldSchema;