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.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
85
vendor/zapier-platform/packages/schema/lib/functional-constraints/AuthFieldisSafe.js
vendored
Normal file
85
vendor/zapier-platform/packages/schema/lib/functional-constraints/AuthFieldisSafe.js
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
'use strict';
|
||||
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const AUTH_FIELD_ID = '/AuthFieldSchema';
|
||||
const AUTH_FIELDS_ID = '/AuthFieldsSchema';
|
||||
|
||||
const FORBIDDEN_KEYS = [
|
||||
'access_token',
|
||||
'access-token',
|
||||
'accesstoken',
|
||||
'api_key',
|
||||
'apikey',
|
||||
'api-key',
|
||||
'auth',
|
||||
'jwt',
|
||||
'passwd',
|
||||
'password',
|
||||
'pswd',
|
||||
'refresh_token',
|
||||
'refresh-token',
|
||||
'refreshtoken',
|
||||
'secret',
|
||||
'set-cookie',
|
||||
'set_cookie',
|
||||
'setcookie',
|
||||
'signature',
|
||||
'token',
|
||||
];
|
||||
|
||||
const isSensitiveKey = (key = '') =>
|
||||
FORBIDDEN_KEYS.some((forbidden) => key.toLowerCase().includes(forbidden));
|
||||
|
||||
const checkAuthField = (field) => {
|
||||
const errors = [];
|
||||
|
||||
// if the field key contains any forbidden substring (case-insensitive),
|
||||
// AND 'isNoSecret' is true, throw a validation error
|
||||
if (
|
||||
(field.key === 'password' || isSensitiveKey(field.key)) &&
|
||||
field.isNoSecret === true
|
||||
) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`cannot set isNoSecret as true for the sensitive key "${field.key}".`,
|
||||
field,
|
||||
'/AuthFieldSchema',
|
||||
'instance.field',
|
||||
'sensitive',
|
||||
'field',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = (definition, mainSchema) => {
|
||||
const errors = [];
|
||||
// Done to validate anti-examples declaratively defined in the schema
|
||||
if ([AUTH_FIELD_ID, AUTH_FIELDS_ID].includes(mainSchema.id)) {
|
||||
const definitions = Array.isArray(definition) ? definition : [definition];
|
||||
definitions.forEach((field, index) => {
|
||||
checkAuthField(field).forEach((err) => {
|
||||
err.property = `instance[${index}]`;
|
||||
err.stack = err.stack.replace('instance.field', err.property);
|
||||
errors.push(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If there's no authentication or no fields, we have nothing to check
|
||||
if (!definition.authentication || !definition.authentication.fields) {
|
||||
return errors;
|
||||
}
|
||||
definition.authentication.fields.forEach((field, index) => {
|
||||
checkAuthField(field).forEach((err) => {
|
||||
err.property = `instance.authentication.fields[${index}]`;
|
||||
err.stack = err.stack.replace('instance.field', err.property);
|
||||
errors.push(err);
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
102
vendor/zapier-platform/packages/schema/lib/functional-constraints/bufferedCreateConstraints.js
vendored
Normal file
102
vendor/zapier-platform/packages/schema/lib/functional-constraints/bufferedCreateConstraints.js
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const bufferedCreateConstraints = (definition) => {
|
||||
const errors = [];
|
||||
const actionType = 'creates';
|
||||
|
||||
if (definition[actionType]) {
|
||||
_.each(definition[actionType], (actionDef) => {
|
||||
if (actionDef.operation && actionDef.operation.buffer) {
|
||||
if (!actionDef.operation.performBuffer) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must contain property "performBuffer" because property "buffer" is present.',
|
||||
actionDef.operation,
|
||||
'/BasicCreateOperationSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation`,
|
||||
'missing',
|
||||
'performBuffer',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (actionDef.operation.perform) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must not contain property "perform" because it is mutually exclusive with property "buffer".',
|
||||
actionDef.operation,
|
||||
'/BasicCreateOperationSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation`,
|
||||
'invalid',
|
||||
'perform',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (actionDef.operation.buffer.groupedBy) {
|
||||
const requiredInputFields = [];
|
||||
const inputFields = _.get(
|
||||
actionDef,
|
||||
['operation', 'inputFields'],
|
||||
[],
|
||||
);
|
||||
inputFields.forEach((inputField) => {
|
||||
if (inputField.required) {
|
||||
requiredInputFields.push(inputField.key);
|
||||
}
|
||||
});
|
||||
|
||||
actionDef.operation.buffer.groupedBy.forEach((field, index) => {
|
||||
if (!requiredInputFields.includes(field)) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`cannot use optional or non-existent inputField "${field}".`,
|
||||
actionDef.operation.buffer,
|
||||
'/BufferConfigSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation.buffer.groupedBy[${index}]`,
|
||||
'invalid',
|
||||
'groupedBy',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actionDef.operation && actionDef.operation.performBuffer) {
|
||||
if (!actionDef.operation.buffer) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must contain property "buffer" because property "performBuffer" is present.',
|
||||
actionDef.operation,
|
||||
'/BasicCreateOperationSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation`,
|
||||
'missing',
|
||||
'buffer',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (actionDef.operation.perform) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must not contain property "perform" because it is mutually exclusive with property "performBuffer".',
|
||||
actionDef.operation,
|
||||
'/BasicCreateOperationSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation`,
|
||||
'invalid',
|
||||
'perform',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = bufferedCreateConstraints;
|
||||
68
vendor/zapier-platform/packages/schema/lib/functional-constraints/deepNestedFields.js
vendored
Normal file
68
vendor/zapier-platform/packages/schema/lib/functional-constraints/deepNestedFields.js
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const collectErrors = (inputFields, path) => {
|
||||
const errors = [];
|
||||
|
||||
_.each(inputFields, (inputField, index) => {
|
||||
if (inputField.children) {
|
||||
if (inputField.children.length === 0) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must not be empty.',
|
||||
inputField,
|
||||
'/PlainFieldSchema',
|
||||
`instance.${path}.inputFields[${index}].children`,
|
||||
'empty',
|
||||
'inputFields',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const hasDeeplyNestedChildren = _.some(
|
||||
inputField.children,
|
||||
(child) => child.children,
|
||||
);
|
||||
|
||||
if (hasDeeplyNestedChildren) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must not contain deeply nested child fields. One level max.',
|
||||
inputField,
|
||||
'/PlainFieldSchema',
|
||||
`instance.${path}.inputFields[${index}]`,
|
||||
'deepNesting',
|
||||
'inputFields',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateFieldNesting = (definition) => {
|
||||
let errors = [];
|
||||
|
||||
_.each(['triggers', 'searches', 'creates'], (typeOf) => {
|
||||
if (definition[typeOf]) {
|
||||
_.each(definition[typeOf], (actionDef) => {
|
||||
if (actionDef.operation && actionDef.operation.inputFields) {
|
||||
errors = errors.concat(
|
||||
collectErrors(
|
||||
actionDef.operation.inputFields,
|
||||
`${typeOf}.${actionDef.key}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = validateFieldNesting;
|
||||
41
vendor/zapier-platform/packages/schema/lib/functional-constraints/index.js
vendored
Normal file
41
vendor/zapier-platform/packages/schema/lib/functional-constraints/index.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use strict';
|
||||
|
||||
/* Each check below is expected to return a list of ValidationSchema errors. An error is defined by:
|
||||
* new jsonschema.ValidationError(
|
||||
* message, // string that explains the problem, like 'must not have a URL that points to AWS'
|
||||
* instance, // the snippet of the app definition that is invalid
|
||||
* schema, // name of the schema that failed, like '/TriggerSchema'
|
||||
* propertyPath, // stringified path to problematic snippet, like 'instance.triggers.find_contact'
|
||||
* name, // optional, the validation type that failed. Can make something up like 'invalidUrl'
|
||||
* argument // optional
|
||||
* );
|
||||
*/
|
||||
const checks = [
|
||||
require('./searchOrCreateKeys'),
|
||||
require('./deepNestedFields'),
|
||||
require('./mutuallyExclusiveFields'),
|
||||
require('./requiredSamples'),
|
||||
require('./matchingKeys'),
|
||||
require('./labelWhenVisible'),
|
||||
require('./uniqueInputFieldKeys'),
|
||||
require('./bufferedCreateConstraints'),
|
||||
require('./requirePerformConditionally'),
|
||||
require('./pollingThrottle'),
|
||||
require('./AuthFieldisSafe'),
|
||||
require('./inputFieldGroupsConstraints'),
|
||||
require('./validateJsonFieldSchema'),
|
||||
];
|
||||
|
||||
const runFunctionalConstraints = (definition, mainSchema) => {
|
||||
return checks.reduce((errors, checkFunc) => {
|
||||
const errorsForCheck = checkFunc(definition, mainSchema);
|
||||
if (errorsForCheck) {
|
||||
errors = errors.concat(errorsForCheck);
|
||||
}
|
||||
return errors;
|
||||
}, []);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
run: runFunctionalConstraints,
|
||||
};
|
||||
144
vendor/zapier-platform/packages/schema/lib/functional-constraints/inputFieldGroupsConstraints.js
vendored
Normal file
144
vendor/zapier-platform/packages/schema/lib/functional-constraints/inputFieldGroupsConstraints.js
vendored
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const actionTypes = ['triggers', 'searches', 'creates', 'bulkReads'];
|
||||
const resourceMethods = ['get', 'list', 'hook', 'search', 'create'];
|
||||
|
||||
const validateInputFieldGroups = (
|
||||
inputFields,
|
||||
inputFieldGroups,
|
||||
basePath,
|
||||
schemaName,
|
||||
) => {
|
||||
const errors = [];
|
||||
|
||||
// Check for duplicate group keys in inputFieldGroups
|
||||
const groupKeys = inputFieldGroups.map((group) => group.key);
|
||||
const duplicateKeys = groupKeys.filter(
|
||||
(key, index) => groupKeys.indexOf(key) !== index,
|
||||
);
|
||||
|
||||
if (duplicateKeys.length > 0) {
|
||||
duplicateKeys.forEach((duplicateKey) => {
|
||||
const duplicateIndex = groupKeys.lastIndexOf(duplicateKey);
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`Duplicate group key "${duplicateKey}" found in inputFieldGroups. Group keys must be unique.`,
|
||||
inputFieldGroups[duplicateIndex],
|
||||
schemaName,
|
||||
`${basePath}.inputFieldGroups[${duplicateIndex}].key`,
|
||||
'duplicateGroupKey',
|
||||
'inputFieldGroups',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Create a set of valid group keys
|
||||
const validGroupKeys = new Set(groupKeys);
|
||||
|
||||
inputFields.forEach((inputField, index) => {
|
||||
// Check children fields first - groups are not allowed in children
|
||||
(inputField.children || []).forEach((childField, childIndex) => {
|
||||
if (childField.group) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`Group fields are not allowed in children fields. Remove the group property from this field.`,
|
||||
childField.group,
|
||||
'/PlainInputFieldSchema',
|
||||
`${basePath}.inputFields[${index}].children[${childIndex}].group`,
|
||||
'groupInChildren',
|
||||
'group',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if group reference is valid
|
||||
if (inputField.group) {
|
||||
if (!validGroupKeys.has(inputField.group)) {
|
||||
const availableGroups =
|
||||
Array.from(validGroupKeys).length > 0
|
||||
? `[${Array.from(validGroupKeys).join(', ')}]`
|
||||
: '[]';
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`Group "${inputField.group}" is not defined in inputFieldGroups. Available groups: ${availableGroups}`,
|
||||
inputField.group,
|
||||
'/PlainInputFieldSchema',
|
||||
`${basePath}.inputFields[${index}].group`,
|
||||
'invalidGroupReference',
|
||||
'group',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const inputFieldGroupsConstraints = (definition) => {
|
||||
const errors = [];
|
||||
|
||||
// Validate action types (triggers, searches, creates, bulkReads)
|
||||
for (const actionType of actionTypes) {
|
||||
const group = definition[actionType] || {};
|
||||
_.each(group, (action, actionKey) => {
|
||||
const inputFields = _.get(action, ['operation', 'inputFields'], []);
|
||||
const inputFieldGroups = _.get(
|
||||
action,
|
||||
['operation', 'inputFieldGroups'],
|
||||
[],
|
||||
);
|
||||
|
||||
const basePath = `instance.${actionType}.${actionKey}.operation`;
|
||||
const schemaName = '/BasicOperationSchema';
|
||||
|
||||
const actionErrors = validateInputFieldGroups(
|
||||
inputFields,
|
||||
inputFieldGroups,
|
||||
basePath,
|
||||
schemaName,
|
||||
);
|
||||
errors.push(...actionErrors);
|
||||
});
|
||||
}
|
||||
|
||||
// Validate resources
|
||||
if (definition.resources) {
|
||||
_.each(definition.resources, (resource, resourceKey) => {
|
||||
resourceMethods.forEach((method) => {
|
||||
if (resource[method] && resource[method].operation) {
|
||||
const inputFields = _.get(
|
||||
resource[method],
|
||||
['operation', 'inputFields'],
|
||||
[],
|
||||
);
|
||||
const inputFieldGroups = _.get(
|
||||
resource[method],
|
||||
['operation', 'inputFieldGroups'],
|
||||
[],
|
||||
);
|
||||
|
||||
const basePath = `instance.resources.${resourceKey}.${method}.operation`;
|
||||
const schemaName = '/BasicOperationSchema';
|
||||
|
||||
const resourceErrors = validateInputFieldGroups(
|
||||
inputFields,
|
||||
inputFieldGroups,
|
||||
basePath,
|
||||
schemaName,
|
||||
);
|
||||
errors.push(...resourceErrors);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = inputFieldGroupsConstraints;
|
||||
31
vendor/zapier-platform/packages/schema/lib/functional-constraints/labelWhenVisible.js
vendored
Normal file
31
vendor/zapier-platform/packages/schema/lib/functional-constraints/labelWhenVisible.js
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const actionTypes = ['triggers', 'searches', 'creates', 'bulkReads'];
|
||||
|
||||
const labelWhenVisible = (definition) => {
|
||||
const errors = [];
|
||||
|
||||
for (const actionType of actionTypes) {
|
||||
const group = definition[actionType] || {};
|
||||
_.each(group, (action, key) => {
|
||||
const { display } = action;
|
||||
if (!display.hidden && !(display.label && display.description)) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`visible actions must have a label and description`,
|
||||
action,
|
||||
`/BasicDisplaySchema`,
|
||||
`instance.${actionType}.${key}.display`,
|
||||
'invalid',
|
||||
'key',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = labelWhenVisible;
|
||||
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/matchingKeys.js
vendored
Normal file
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/matchingKeys.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const actionTypes = ['triggers', 'searches', 'creates'];
|
||||
|
||||
const matchingKeys = (definition) => {
|
||||
const errors = [];
|
||||
|
||||
// verifies that x.key === x
|
||||
// otherwise, we double results in the compiled app via core's compileApp
|
||||
|
||||
for (const actionType of actionTypes) {
|
||||
const group = definition[actionType] || {};
|
||||
_.each(group, (action, key) => {
|
||||
if (action.key !== key) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must have a matching top-level key (found "${key}" and "${action.key}")`,
|
||||
action,
|
||||
`/${_.capitalize(actionType)}Schema`,
|
||||
`instance.${key}.key`,
|
||||
'invalid',
|
||||
'key',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = matchingKeys;
|
||||
70
vendor/zapier-platform/packages/schema/lib/functional-constraints/mutuallyExclusiveFields.js
vendored
Normal file
70
vendor/zapier-platform/packages/schema/lib/functional-constraints/mutuallyExclusiveFields.js
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
// NOTE: While it would be possible to accomplish this with a solution like
|
||||
// https://stackoverflow.com/questions/28162509/mutually-exclusive-property-groups#28172831
|
||||
// it was harder to read and understand.
|
||||
|
||||
const {
|
||||
INCOMPATIBLE_FIELD_SCHEMA_KEYS,
|
||||
FIELD_SCHEMA_BOOLEANS,
|
||||
} = require('../constants');
|
||||
|
||||
const verifyIncompatibilities = (inputFields, path) => {
|
||||
const errors = [];
|
||||
|
||||
_.each(inputFields, (inputField, index) => {
|
||||
_.each(INCOMPATIBLE_FIELD_SCHEMA_KEYS, ([firstField, secondField]) => {
|
||||
if (_.has(inputField, firstField) && _.has(inputField, secondField)) {
|
||||
// this could be ok if it's a boolean field and is falsy
|
||||
// i'm reasonably sure that the editor handles this fine, but if it also checks
|
||||
// for the existence of the property (not the truthiness), then there could be an issue
|
||||
if (
|
||||
(FIELD_SCHEMA_BOOLEANS.has(firstField) && !inputField[firstField]) ||
|
||||
(FIELD_SCHEMA_BOOLEANS.has(secondField) && !inputField[secondField])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must not contain ${firstField} and ${secondField}, as they're mutually exclusive.`,
|
||||
inputField,
|
||||
'/PlainFieldSchema',
|
||||
`instance.${path}.inputFields[${index}]`,
|
||||
'invalid',
|
||||
'inputFields',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const mutuallyExclusiveFields = (definition) => {
|
||||
let errors = [];
|
||||
|
||||
_.each(['triggers', 'searches', 'creates'], (typeOf) => {
|
||||
if (definition[typeOf]) {
|
||||
_.each(definition[typeOf], (actionDef) => {
|
||||
if (actionDef.operation && actionDef.operation.inputFields) {
|
||||
errors = [
|
||||
...errors,
|
||||
...verifyIncompatibilities(
|
||||
actionDef.operation.inputFields,
|
||||
`${typeOf}.${actionDef.key}`,
|
||||
),
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = mutuallyExclusiveFields;
|
||||
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/pollingThrottle.js
vendored
Normal file
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/pollingThrottle.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const pollingThrottle = (definition) => {
|
||||
const errors = [];
|
||||
const actionType = 'triggers';
|
||||
|
||||
if (definition[actionType]) {
|
||||
_.each(definition[actionType], (actionDef) => {
|
||||
if (
|
||||
actionDef.operation &&
|
||||
actionDef.operation.throttle &&
|
||||
_.has(actionDef.operation.throttle, 'retry') &&
|
||||
(!actionDef.operation.type || actionDef.operation.type === 'polling')
|
||||
) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'must not use the "retry" field for a polling trigger.',
|
||||
actionDef.operation.throttle,
|
||||
'/ThrottleObjectSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation.throttle`,
|
||||
'invalid',
|
||||
'throttle',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = pollingThrottle;
|
||||
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/requirePerformConditionally.js
vendored
Normal file
35
vendor/zapier-platform/packages/schema/lib/functional-constraints/requirePerformConditionally.js
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const requirePerformConditionally = (definition) => {
|
||||
const errors = [];
|
||||
const actionType = 'creates';
|
||||
|
||||
if (definition[actionType]) {
|
||||
_.each(definition[actionType], (actionDef) => {
|
||||
if (
|
||||
actionDef.operation &&
|
||||
!actionDef.operation.buffer &&
|
||||
!actionDef.operation.performBuffer &&
|
||||
!actionDef.operation.perform
|
||||
) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
'requires property "perform".',
|
||||
actionDef.operation,
|
||||
'/BasicCreateOperationSchema',
|
||||
`instance.${actionType}.${actionDef.key}.operation`,
|
||||
'required',
|
||||
'perform',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = requirePerformConditionally;
|
||||
56
vendor/zapier-platform/packages/schema/lib/functional-constraints/requiredSamples.js
vendored
Normal file
56
vendor/zapier-platform/packages/schema/lib/functional-constraints/requiredSamples.js
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
// todo: deal with circular dep.
|
||||
const RESOURCE_ID = '/ResourceSchema';
|
||||
const RESOURCE_METHODS = ['get', 'hook', 'list', 'search', 'create'];
|
||||
|
||||
const check = (definition) => {
|
||||
if (!definition.operation || _.get(definition, 'display.hidden')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const samples = _.get(definition, 'operation.sample', {});
|
||||
return !_.isEmpty(samples)
|
||||
? null
|
||||
: new jsonschema.ValidationError(
|
||||
'requires "sample", because it\'s not hidden',
|
||||
definition,
|
||||
definition.id,
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = (definition, mainSchema) => {
|
||||
let definitions = [];
|
||||
|
||||
if (mainSchema.id === RESOURCE_ID) {
|
||||
definitions = RESOURCE_METHODS.map((method) => definition[method]).filter(
|
||||
Boolean,
|
||||
);
|
||||
|
||||
// allow method definitions to inherit the sample
|
||||
if (definition.sample) {
|
||||
definitions.forEach((methodDefinition) => {
|
||||
if (methodDefinition.operation && !methodDefinition.operation.sample) {
|
||||
methodDefinition.operation.sample = definition.sample;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!definitions.length) {
|
||||
return [
|
||||
new jsonschema.ValidationError(
|
||||
'expected at least one resource operation',
|
||||
definition,
|
||||
definition.id,
|
||||
),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
definitions = [definition];
|
||||
}
|
||||
|
||||
return definitions.map(check).filter(Boolean);
|
||||
};
|
||||
263
vendor/zapier-platform/packages/schema/lib/functional-constraints/searchOrCreateKeys.js
vendored
Normal file
263
vendor/zapier-platform/packages/schema/lib/functional-constraints/searchOrCreateKeys.js
vendored
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const getFieldKeys = (definition, path) => {
|
||||
const fields = _.get(definition, path, []);
|
||||
// Filter out any `undefined` values using .filter(), which may happen due to incoming inputFields
|
||||
// containing functions instead of plain Objects.
|
||||
return fields.map((field) => field.key).filter((key) => key);
|
||||
};
|
||||
|
||||
// This method differs from 'getFieldKeys' since here we obtain the actual object keys with Object.keys()
|
||||
const getSearchOutputSampleKeys = (definition, searchKey) => {
|
||||
const searchOutputSampleFields = _.get(
|
||||
definition.searches,
|
||||
`${searchKey}.operation.sample`,
|
||||
{},
|
||||
);
|
||||
|
||||
return Object.keys(searchOutputSampleFields);
|
||||
};
|
||||
|
||||
const validateSearchCreateKeys = (definition, searchCreatesKey) => {
|
||||
const searchCreates = definition[searchCreatesKey];
|
||||
|
||||
if (!searchCreates) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
|
||||
const searchKeys = Object.keys(definition.searches);
|
||||
const createKeys = Object.keys(definition.creates);
|
||||
|
||||
_.each(searchCreates, (searchOrCreateDef, key) => {
|
||||
const searchOrCreateKey = searchOrCreateDef.key;
|
||||
const searchKey = searchOrCreateDef.search;
|
||||
const createKey = searchOrCreateDef.create;
|
||||
const updateKey = searchOrCreateDef.update;
|
||||
|
||||
const updateInputKeys = getFieldKeys(
|
||||
definition.creates,
|
||||
`${updateKey}.operation.inputFields`,
|
||||
);
|
||||
const searchInputKeys = getFieldKeys(
|
||||
definition.searches,
|
||||
`${searchKey}.operation.inputFields`,
|
||||
);
|
||||
const searchOutputKeys = getFieldKeys(
|
||||
definition.searches,
|
||||
`${searchKey}.operation.outputFields`,
|
||||
);
|
||||
const searchOutputSampleKeys = getSearchOutputSampleKeys(
|
||||
definition,
|
||||
searchKey,
|
||||
);
|
||||
|
||||
// There are constraints where we check for keys in either outputFields or sample, so combining them is a shortcut
|
||||
const allSearchOutputKeys = new Set([
|
||||
...searchOutputKeys,
|
||||
...searchOutputSampleKeys,
|
||||
]);
|
||||
|
||||
// For some constraints, there is a difference between not "having" a key defined versus having one but with empty values
|
||||
const hasSearchOutputFields = _.has(
|
||||
definition.searches,
|
||||
`${searchKey}.operation.outputFields`,
|
||||
);
|
||||
const hasSearchOutputSample = _.has(
|
||||
definition.searches,
|
||||
`${searchKey}.operation.sample`,
|
||||
);
|
||||
|
||||
// Confirm searchOrCreate.key matches a searches.key (current Zapier editor limitation)
|
||||
if (!definition.searches[searchOrCreateKey]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must match a "key" from a search (options: ${searchKeys})`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.key`,
|
||||
'invalidKey',
|
||||
'key',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.search matches a searches.key
|
||||
if (!definition.searches[searchKey]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must match a "key" from a search (options: ${searchKeys})`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.search`,
|
||||
'invalidKey',
|
||||
'search',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.create matches a creates.key
|
||||
if (!definition.creates[createKey]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must match a "key" from a create (options: ${createKeys})`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.create`,
|
||||
'invalidKey',
|
||||
'create',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.update matches a creates.key, if it is defined
|
||||
if (updateKey && !definition.creates[updateKey]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`must match a "key" from a create (options: ${createKeys})`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.update`,
|
||||
'invalidKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.updateInputFromSearchOutput existing implies searchOrCreate.update is defined
|
||||
if (searchOrCreateDef.updateInputFromSearchOutput && !updateKey) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`requires searchOrCreates.${key}.update to be defined`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.updateInputFromSearchOutput`,
|
||||
'invalid',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.searchUniqueInputToOutputConstraint existing implies searchOrCreate.update is defined
|
||||
if (searchOrCreateDef.searchUniqueInputToOutputConstraint && !updateKey) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`requires searchOrCreates.${key}.update to be defined`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.searchUniqueInputToOutputConstraint`,
|
||||
'invalid',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.updateInputFromSearchOutput contains objects with:
|
||||
// keys existing in creates[update].operation.inputFields.key
|
||||
// values existing in searches[search].operation.(outputFields.key|sample keys), if they are defined
|
||||
if (
|
||||
updateKey &&
|
||||
_.isPlainObject(searchOrCreateDef.updateInputFromSearchOutput)
|
||||
) {
|
||||
const updateInputOptionHint = _.isEmpty(updateInputKeys)
|
||||
? '(no "key" found in inputFields)'
|
||||
: `(options: ${updateInputKeys})`;
|
||||
|
||||
const searchOutputOptionHint = `(options: ${[...allSearchOutputKeys]})`;
|
||||
|
||||
for (const [updateInputField, searchOutputField] of Object.entries(
|
||||
searchOrCreateDef.updateInputFromSearchOutput,
|
||||
)) {
|
||||
if (!updateInputKeys.includes(updateInputField)) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`object key must match a "key" from a creates.${updateKey}.operation.inputFields ${updateInputOptionHint}`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.updateInputFromSearchOutput`,
|
||||
'invalidKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(hasSearchOutputFields || hasSearchOutputSample) &&
|
||||
!allSearchOutputKeys.has(searchOutputField)
|
||||
) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`object value must match a "key" from searches.${searchKey}.operation.(outputFields|sample) ${searchOutputOptionHint}`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.updateInputFromSearchOutput`,
|
||||
'invalidKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm searchOrCreate.searchUniqueInputToOutputConstraint contains objects with:
|
||||
// keys existing in searches[search].operation.inputFields.key
|
||||
// values existing in searches[search].operation.(outputFields.key|sample keys), if they are defined
|
||||
if (
|
||||
updateKey &&
|
||||
_.isPlainObject(searchOrCreateDef.searchUniqueInputToOutputConstraint)
|
||||
) {
|
||||
const searchInputOptionHint = _.isEmpty(searchInputKeys)
|
||||
? '(no "key" found in inputFields)'
|
||||
: `(options: ${searchInputKeys})`;
|
||||
|
||||
const searchOutputOptionHint = `(options: ${[...allSearchOutputKeys]})`;
|
||||
|
||||
for (const [searchInputField, searchOutputField] of Object.entries(
|
||||
searchOrCreateDef.searchUniqueInputToOutputConstraint,
|
||||
)) {
|
||||
if (!searchInputKeys.includes(searchInputField)) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`object key must match a "key" from a searches.${searchKey}.operation.inputFields ${searchInputOptionHint}`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.searchUniqueInputToOutputConstraint`,
|
||||
'invalidKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(hasSearchOutputFields || hasSearchOutputSample) &&
|
||||
typeof searchOutputField === 'string' &&
|
||||
!allSearchOutputKeys.has(searchOutputField)
|
||||
) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`object value must match a "key" from searches.${searchKey}.operation.(outputFields|sample) ${searchOutputOptionHint}`,
|
||||
searchOrCreateDef,
|
||||
'/SearchOrCreateSchema',
|
||||
`instance.${searchCreatesKey}.${key}.searchUniqueInputToOutputConstraint`,
|
||||
'invalidKey',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateSearchOrCreateKeys = (definition) => {
|
||||
// searchAndCreates is an alias for searchOrCreates. They are the same, but
|
||||
// you can define both searchAndCreates and searchOrCreates to avoid search
|
||||
// key collision.
|
||||
return [
|
||||
...validateSearchCreateKeys(definition, 'searchOrCreates'),
|
||||
...validateSearchCreateKeys(definition, 'searchAndCreates'),
|
||||
];
|
||||
};
|
||||
|
||||
module.exports = validateSearchOrCreateKeys;
|
||||
66
vendor/zapier-platform/packages/schema/lib/functional-constraints/uniqueInputFieldKeys.js
vendored
Normal file
66
vendor/zapier-platform/packages/schema/lib/functional-constraints/uniqueInputFieldKeys.js
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const jsonschema = require('jsonschema');
|
||||
|
||||
const actionTypes = ['triggers', 'searches', 'creates'];
|
||||
|
||||
const uniqueInputFieldKeys = (definition) => {
|
||||
const errors = [];
|
||||
for (const actionType of actionTypes) {
|
||||
const group = definition[actionType] || {};
|
||||
_.each(group, (action, key) => {
|
||||
const inputFields = _.get(action, ['operation', 'inputFields'], []);
|
||||
|
||||
const existingKeys = {}; // map of key to where it already lives
|
||||
|
||||
inputFields.forEach((inputField, index) => {
|
||||
// could be a string or a non-field object (`source` or `require` function obj)
|
||||
if (!inputField.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingKeys[inputField.key]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`inputField keys must be unique for each action. The key "${
|
||||
inputField.key
|
||||
}" is already in use at ${actionType}.${key}.operation.${
|
||||
existingKeys[inputField.key]
|
||||
}.key`,
|
||||
inputField.key,
|
||||
`/BasicOperationSchema`,
|
||||
`instance.${actionType}.${key}.operation.inputFields[${index}].key`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
existingKeys[inputField.key] = `inputFields[${index}]`;
|
||||
}
|
||||
|
||||
(inputField.children || []).forEach((subField, subFieldIndex) => {
|
||||
if (existingKeys[subField.key]) {
|
||||
errors.push(
|
||||
new jsonschema.ValidationError(
|
||||
`inputField keys must be unique for each action, even if they're children. The key "${
|
||||
subField.key
|
||||
}" is already in use at ${actionType}.${key}.operation.${
|
||||
existingKeys[subField.key]
|
||||
}.key`,
|
||||
subField.key,
|
||||
`/BasicOperationSchema`,
|
||||
`instance.${actionType}.${key}.operation.inputFields[${index}].children[${subFieldIndex}].key`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
existingKeys[subField.key] =
|
||||
`inputFields[${index}].children[${subFieldIndex}]`;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
module.exports = uniqueInputFieldKeys;
|
||||
254
vendor/zapier-platform/packages/schema/lib/functional-constraints/validateJsonFieldSchema.js
vendored
Normal file
254
vendor/zapier-platform/packages/schema/lib/functional-constraints/validateJsonFieldSchema.js
vendored
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
'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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue