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.
254 lines
8.3 KiB
JavaScript
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;
|