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:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View file

@ -0,0 +1,21 @@
module.exports = {
ROOT_GITHUB: 'https://github.com/zapier/zapier-platform',
DOCS_PATH: 'docs/build/schema.md',
DOC_URL_TEMPLATE:
'https://github.com/zapier/zapier-platform/blob/zapier-platform-cli%40<%= version %>/packages/schema/docs/build/schema.md<%= anchor %>',
SKIP_KEY: '_skipTest',
// the following pairs of keys can't be used together in PlainFieldSchema
// they're stored here because they're used in a few places
INCOMPATIBLE_FIELD_SCHEMA_KEYS: [
['children', 'list'], // This is actually a Feature Request (https://github.com/zapier/zapier-platform-cli/issues/115)
['children', 'dict'], // dict is ignored
['children', 'type'], // type is ignored
['children', 'placeholder'], // placeholder is ignored
['children', 'helpText'], // helpText is ignored
['children', 'default'], // default is ignored
['dict', 'list'], // Use only one or the other
['dynamic', 'dict'], // dict is ignored
['dynamic', 'choices'], // choices are ignored
],
FIELD_SCHEMA_BOOLEANS: new Set(['dict', 'list']),
};

View 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;
};

View 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;

View 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;

View 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,
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);
};

View 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;

View 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;

View 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;

View file

@ -0,0 +1,52 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/AppFlagsSchema',
description: 'Codifies high-level options for your integration.',
type: 'object',
properties: {
skipHttpPatch: {
description:
"By default, Zapier patches the core `http` module so that all requests (including those from 3rd-party SDKs) can be logged. Set this to true if you're seeing issues using an SDK (such as AWS).",
type: 'boolean',
},
skipThrowForStatus: {
description:
'Starting in `core` version `10.0.0`, `response.throwForStatus()` was called by default. We introduced a per-request way to opt-out of this behavior. This flag takes that a step further and controls that behavior integration-wide **for requests made using `z.request()`**. Unless they specify otherwise (per-request, or via middleware), [Shorthand requests](https://github.com/zapier/zapier-platform/blob/main/packages/cli/README.md#shorthand-http-requests) _always_ call `throwForStatus()`. `z.request()` calls can also ignore this flag if they set `skipThrowForStatus` directly. It is important to note that for oauth2 or session auths with `authRefresh:true`, `401` status codes will throw a `RefreshAuthError` regardless of `skipThrowForStatus`, and will need to be handled manually if intervention is required.',
type: 'boolean',
},
throwForThrottlingEarly: {
description:
'Starting in `core` version `18.0.0`, 429 (throttling) responses throw a `ThrottledError` before `afterResponse` middleware runs by default. Set this flag to `true` to preserve the old behavior where `afterResponse` middleware can see and handle 429 responses. This flag can be overridden per-request by setting `throwForThrottlingEarly` directly on the request options.',
type: 'boolean',
},
cleanInputData: {
description:
'If true, Zapier removes empty strings, `null`, `undefined`, and empty Arrays or objects from `bundle.inputData` recursively before passing it to your `perform*` function. If you want to handle empty values yourself in your code, explicitly set this to false. This is a global flag that affects all the triggers and actions in your integration. The `cleanInputData` flag in `operation` takes precedence over this one.',
type: 'boolean',
},
},
additionalProperties: false,
examples: [
{
skipHttpPatch: true,
skipThrowForStatus: false,
throwForThrottlingEarly: true,
},
{
skipHttpPatch: false,
skipThrowForStatus: true,
throwForThrottlingEarly: false,
},
{ throwForThrottlingEarly: true },
{},
],
antiExamples: [
{ example: { foo: true }, reason: 'Invalid key.' },
{ example: { skipHttpPatch: 'yes' }, reason: 'Invalid value.' },
{ example: { skipThrowForStatus: 'no' }, reason: 'Invalid value.' },
{ example: { throwForThrottlingEarly: 'yes' }, reason: 'Invalid value.' },
],
});

View file

@ -0,0 +1,160 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const AuthenticationSchema = require('./AuthenticationSchema');
const FlatObjectSchema = require('./FlatObjectSchema');
const ResourcesSchema = require('./ResourcesSchema');
const TriggersSchema = require('./TriggersSchema');
const ReadBulksSchema = require('./BulkReadsSchema');
const SearchesSchema = require('./SearchesSchema');
const CreatesSchema = require('./CreatesSchema');
const SearchOrCreatesSchema = require('./SearchOrCreatesSchema');
const SearchAndCreatesSchema = require('./SearchAndCreatesSchema');
const RequestSchema = require('./RequestSchema');
const VersionSchema = require('./VersionSchema');
const MiddlewaresSchema = require('./MiddlewaresSchema');
const HydratorsSchema = require('./HydratorsSchema');
const AppFlagsSchema = require('./AppFlagsSchema');
const ThrottleObjectSchema = require('./ThrottleObjectSchema');
module.exports = makeSchema(
{
id: '/AppSchema',
description: 'Represents a full app.',
type: 'object',
required: ['version', 'platformVersion'],
properties: {
version: {
description: 'A version identifier for your code.',
$ref: VersionSchema.id,
},
platformVersion: {
description:
'A version identifier for the Zapier execution environment.',
$ref: VersionSchema.id,
},
beforeApp: {
description:
'EXPERIMENTAL: Before the perform method is called on your app, you can modify the execution context.',
$ref: MiddlewaresSchema.id,
},
afterApp: {
description:
'EXPERIMENTAL: After the perform method is called on your app, you can modify the response.',
$ref: MiddlewaresSchema.id,
},
authentication: {
description: 'Choose what scheme your API uses for authentication.',
$ref: AuthenticationSchema.id,
},
requestTemplate: {
description:
'Define a request mixin, great for setting custom headers, content-types, etc.',
$ref: RequestSchema.id,
},
beforeRequest: {
description:
'Before an HTTP request is sent via our `z.request()` client, you can modify it.',
$ref: MiddlewaresSchema.id,
},
afterResponse: {
description:
'After an HTTP response is recieved via our `z.request()` client, you can modify it.',
$ref: MiddlewaresSchema.id,
},
hydrators: {
description:
"An optional bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.",
$ref: HydratorsSchema.id,
},
resources: {
description:
'All the resources for your app. Zapier will take these and generate the relevent triggers/searches/creates automatically.',
$ref: ResourcesSchema.id,
},
triggers: {
description:
'All the triggers for your app. You can add your own here, or Zapier will automatically register any from the list/hook methods on your resources.',
$ref: TriggersSchema.id,
},
bulkReads: {
description:
'All of the read bulks (GETs) your app exposes to retrieve resources in batches.',
$ref: ReadBulksSchema.id,
},
searches: {
description:
'All the searches for your app. You can add your own here, or Zapier will automatically register any from the search method on your resources.',
$ref: SearchesSchema.id,
},
creates: {
description:
'All the creates for your app. You can add your own here, or Zapier will automatically register any from the create method on your resources.',
$ref: CreatesSchema.id,
},
searchOrCreates: {
description:
'All the search-or-create combos for your app. You can create your own here, or Zapier will automatically register any from resources that define a search, a create, and a get (or define a searchOrCreate directly). Register non-resource search-or-creates here as well.',
$ref: SearchOrCreatesSchema.id,
},
searchAndCreates: {
description: 'An alias for "searchOrCreates".',
$ref: SearchAndCreatesSchema.id,
},
flags: {
description: 'Top-level app options',
$ref: AppFlagsSchema.id,
},
throttle: {
description: `Zapier uses this configuration to apply throttling when the limit for the window is exceeded. When set here, it is the default throttle configuration used on each action of the integration. And when set in an action's operation object, it gets overwritten for that action only.`,
$ref: ThrottleObjectSchema.id,
},
legacy: {
description:
'**INTERNAL USE ONLY**. Zapier uses this to hold properties from a legacy Web Builder app.',
type: 'object',
docAnnotation: {
hide: true,
},
},
firehoseWebhooks: {
description:
'**INTERNAL USE ONLY**. Zapier uses this for internal webhook app configurations.',
type: 'object',
docAnnotation: {
hide: true,
},
},
},
additionalProperties: false,
examples: [{ version: '1.0.0', platformVersion: '10.1.2' }],
antiExamples: [
{
example: { version: 'v1.0.0', platformVersion: '10.1.2' },
reason: 'Invalid value for version.',
},
{
example: { version: '1.0.0', platformVersion: 'v10.1.2' },
reason: 'Invalid value for platformVersion.',
},
],
},
[
AuthenticationSchema,
FlatObjectSchema,
ResourcesSchema,
ReadBulksSchema,
TriggersSchema,
SearchesSchema,
CreatesSchema,
SearchOrCreatesSchema,
SearchAndCreatesSchema,
RequestSchema,
VersionSchema,
MiddlewaresSchema,
HydratorsSchema,
AppFlagsSchema,
ThrottleObjectSchema,
],
);

View file

@ -0,0 +1,131 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FieldChoicesSchema = require('./FieldChoicesSchema');
const PlainFieldSchema = require('./PlainFieldSchema');
module.exports = makeSchema(
{
id: '/AuthFieldSchema',
description: `Field schema specialized for authentication fields. ${PlainFieldSchema.schema.description}`,
required: ['key'],
type: 'object',
properties: {
...PlainFieldSchema.schema.properties,
children: {
type: 'array',
items: { $ref: '/AuthFieldSchema' },
description:
'An array of child fields that define the structure of a sub-object for this field. Usually used for line items.',
minItems: 1,
},
helpText: {
description:
'A human readable description of this value (IE: "The first part of a full name."). You can use Markdown.',
type: 'string',
minLength: 1,
maxLength: 1000,
},
type: {
description: 'The type of this value used to be.',
type: 'string',
enum: [
'string',
'number',
'boolean',
'datetime',
'copy',
'password',
'integer',
'text',
],
},
required: {
description:
'If this value is required or not. This defaults to `true`.',
type: 'boolean',
},
placeholder: {
description: 'An example value that is not saved.',
type: 'string',
minLength: 1,
},
choices: {
description:
'An object of machine keys and human values to populate a static dropdown.',
$ref: FieldChoicesSchema.id,
},
computed: {
description:
'Is this field automatically populated (and hidden from the user)? Note: Only OAuth and Session Auth support fields with this key.',
type: 'boolean',
},
inputFormat: {
description:
'Useful when you expect the input to be part of a longer string. Put "{{input}}" in place of the user\'s input (IE: "https://{{input}}.yourdomain.com").',
type: 'string',
// TODO: Check if it contains one and ONLY ONE '{{input}}'
pattern: '^.*{{input}}.*$',
},
isNoSecret: {
description:
'Indicates if this authentication field is safe to e.g. be stored without encryption or displayed (not a secret).',
type: 'boolean',
},
},
// Add examples and anti-examples specifically for basic & custom auth
examples: [
// Basic Auth - email & password
{
key: 'email',
type: 'string',
isNoSecret: true,
required: true,
},
{
key: 'password',
type: 'password',
isNoSecret: false,
required: true,
},
// Custom Auth - api key
{
key: 'api_key',
type: 'string',
isNoSecret: false,
required: true,
},
],
antiExamples: [
{
example: {
key: 'password',
type: 'password',
isNoSecret: true,
required: true,
},
reason:
'"password" is a sensitive field and cannot have isNoSecret set as true.',
},
{
example: {
key: 'api_key',
isNoSecret: true,
},
reason:
'"api_key" is a sensitive field and cannot have isNoSecret set as true.',
},
{
example: {
type: 'string',
isNoSecret: false,
},
reason: 'Missing required key: key',
},
],
},
[FieldChoicesSchema, PlainFieldSchema],
);

View file

@ -0,0 +1,67 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const AuthFieldSchema = require('./AuthFieldSchema');
module.exports = makeSchema(
{
id: '/AuthFieldsSchema',
description: 'An array or collection of authentication fields.',
type: 'array',
items: {
oneOf: [{ $ref: AuthFieldSchema.id }],
},
// Some complete examples
examples: [
// 1) Basic Auth: username (safe) + password (not safe)
[
{ key: 'username', type: 'string', isNoSecret: true, required: true },
{
key: 'password',
type: 'password',
isNoSecret: false,
required: true,
},
],
// 2) Just a single auth field for custom usage (e.g., api_key)
[{ key: 'api_key', type: 'string', isNoSecret: false, required: true }],
// 3) Mix of fields for extended usage
[
{ key: 'email', type: 'string', isNoSecret: true },
{ key: 'password', type: 'password', required: true },
{ key: 'mfa_token', type: 'string', isNoSecret: false },
],
],
// Anti-examples showing invalid arrays or invalid field objects
antiExamples: [
{
example: {},
reason: 'Must be an array (currently an object).',
},
{
example: [{ key: 'password', isNoSecret: true }],
reason:
'"password" is a sensitive field and cannot have isNoSecret set as true.',
},
{
example: [{ key: 'api_key', isNoSecret: true }],
reason:
'"api_key" is a sensitive field and cannot have isNoSecret set as true.',
},
{
example: [{ isNoSecret: false }],
reason: 'Missing required "key" property.',
},
{
example: [{ key: 'username', type: 'string', isNoSecret: true }, 12345],
reason:
'Array item 12345 is not an object (must match AuthFieldSchema).',
},
],
},
[AuthFieldSchema],
);

View file

@ -0,0 +1,14 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/AuthenticationBasicConfigSchema',
description:
'Config for Basic Authentication. No extra properties are required to setup Basic Auth, so you can leave this empty if your app uses Basic Auth.',
type: 'object',
properties: {},
additionalProperties: false,
examples: [{}],
antiExamples: [{ example: { foo: true }, reason: 'Invalid key.' }],
});

View file

@ -0,0 +1,37 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
module.exports = makeSchema(
{
id: '/AuthenticationCustomConfigSchema',
description:
'Config for custom authentication (like API keys). No extra properties are required to setup this auth type, so you can leave this empty if your app uses a custom auth method.',
type: 'object',
properties: {
sendCode: {
description:
'EXPERIMENTAL: Define the call Zapier should make to send the OTP code.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
},
additionalProperties: false,
examples: [
{},
{
sendCode: {
url: 'https://example.com/api/send',
headers: { Authorization: `Bearer {{process.env.API_KEY}}` },
body: {
to_phone_number: '{{bundle.inputData.phone_number}}',
code: '{{bundle.inputData.code}}',
},
},
},
],
antiExamples: [{ example: { foo: true }, reason: 'Invalid key.' }],
},
[RequestSchema, FunctionSchema],
);

View file

@ -0,0 +1,14 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/AuthenticationDigestConfigSchema',
description:
'Config for Digest Authentication. No extra properties are required to setup Digest Auth, so you can leave this empty if your app uses Digets Auth.',
type: 'object',
properties: {},
additionalProperties: false,
examples: [{}],
antiExamples: [{ example: { foo: true }, reason: 'Invalid key.' }],
});

View file

@ -0,0 +1,53 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
const RedirectRequestSchema = require('./RedirectRequestSchema');
const RequestSchema = require('./RequestSchema');
module.exports = makeSchema(
{
id: '/AuthenticationOAuth1ConfigSchema',
description: 'Config for OAuth1 authentication.',
type: 'object',
required: ['getRequestToken', 'authorizeUrl', 'getAccessToken'],
properties: {
getRequestToken: {
description:
'Define where Zapier will acquire a request token which is used for the rest of the three legged authentication process.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
authorizeUrl: {
description:
'Define where Zapier will redirect the user to authorize our app. Typically, you should append an `oauth_token` querystring parameter to the request.',
oneOf: [
{ $ref: RedirectRequestSchema.id },
{ $ref: FunctionSchema.id },
],
},
getAccessToken: {
description: 'Define how Zapier fetches an access token from the API',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
},
additionalProperties: false,
examples: [
{
getRequestToken: { require: 'some/path/to/file.js' },
authorizeUrl: { require: 'some/path/to/file2.js' },
getAccessToken: { require: 'some/path/to/file3.js' },
},
],
antiExamples: [
{
example: {
getRequestToken: { require: 'some/path/to/file.js' },
authorizeUrl: { require: 'some/path/to/file2.js' },
},
reason: 'Missing required key.',
},
],
},
[FunctionSchema, RedirectRequestSchema, RequestSchema],
);

View file

@ -0,0 +1,78 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
const RedirectRequestSchema = require('./RedirectRequestSchema');
const RequestSchema = require('./RequestSchema');
module.exports = makeSchema(
{
id: '/AuthenticationOAuth2ConfigSchema',
description: 'Config for OAuth2 authentication.',
type: 'object',
required: ['authorizeUrl', 'getAccessToken'],
properties: {
authorizeUrl: {
description:
'Define where Zapier will redirect the user to authorize our app. Note: we append the redirect URL and state parameters to return value of this function.',
oneOf: [
{ $ref: RedirectRequestSchema.id },
{ $ref: FunctionSchema.id },
],
},
getAccessToken: {
description: 'Define how Zapier fetches an access token from the API',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
refreshAccessToken: {
description:
'Define how Zapier will refresh the access token from the API',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
codeParam: {
description:
'Define a non-standard code param Zapier should scrape instead.',
type: 'string',
},
scope: {
description: 'What scope should Zapier request?',
type: 'string',
},
autoRefresh: {
description:
'Should Zapier invoke `refreshAccessToken` when we receive an error for a 401 response?',
type: 'boolean',
},
enablePkce: {
description: 'Should Zapier use PKCE for OAuth2?',
type: 'boolean',
},
},
additionalProperties: false,
examples: [
{
authorizeUrl: { require: 'some/path/to/file.js' },
getAccessToken: { require: 'some/path/to/file2.js' },
},
{
authorizeUrl: { require: 'some/path/to/file.js' },
getAccessToken: { require: 'some/path/to/file2.js' },
refreshAccessToken: { require: 'some/path/to/file3.js' },
codeParam: 'unique_code',
scope: 'read/write',
autoRefresh: true,
enablePkce: true,
},
],
antiExamples: [
{
example: {
authorizeUrl: { require: 'some/path/to/file.js' },
},
reason: 'Missing required key getAccessToken.',
},
],
},
[FunctionSchema, RedirectRequestSchema, RequestSchema],
);

View file

@ -0,0 +1,118 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const AuthenticationBasicConfigSchema = require('./AuthenticationBasicConfigSchema.js');
const AuthenticationCustomConfigSchema = require('./AuthenticationCustomConfigSchema.js');
const AuthenticationDigestConfigSchema = require('./AuthenticationDigestConfigSchema.js');
const AuthenticationOAuth1ConfigSchema = require('./AuthenticationOAuth1ConfigSchema.js');
const AuthenticationOAuth2ConfigSchema = require('./AuthenticationOAuth2ConfigSchema.js');
const AuthenticationSessionConfigSchema = require('./AuthenticationSessionConfigSchema.js');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
const AuthFieldsSchema = require('./AuthFieldsSchema');
module.exports = makeSchema(
{
id: '/AuthenticationSchema',
description: 'Represents authentication schemes.',
type: 'object',
required: ['type', 'test'],
properties: {
type: {
description: 'Choose which scheme you want to use.',
type: 'string',
enum: ['basic', 'custom', 'digest', 'oauth1', 'oauth2', 'session'],
},
test: {
description:
'A function or request that confirms the authentication is working.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
fields: {
description:
'Fields you can request from the user before they connect your app to Zapier.',
$ref: AuthFieldsSchema.id,
},
connectionLabel: {
description:
'A string with variables, function, or request that returns the connection label for the authenticated user.',
anyOf: [
{ $ref: RequestSchema.id },
{ $ref: FunctionSchema.id },
{ type: 'string' },
],
},
// this is preferred to laying out config: anyOf: [...]
basicConfig: { $ref: AuthenticationBasicConfigSchema.id },
customConfig: { $ref: AuthenticationCustomConfigSchema.id },
digestConfig: { $ref: AuthenticationDigestConfigSchema.id },
oauth1Config: { $ref: AuthenticationOAuth1ConfigSchema.id },
oauth2Config: { $ref: AuthenticationOAuth2ConfigSchema.id },
sessionConfig: { $ref: AuthenticationSessionConfigSchema.id },
},
additionalProperties: false,
examples: [
{
type: 'basic',
test: '$func$2$f$',
},
{
type: 'custom',
test: '$func$2$f$',
fields: [{ key: 'abc' }],
},
{
type: 'custom',
test: '$func$2$f$',
connectionLabel: '{{bundle.inputData.abc}}',
},
{
type: 'custom',
test: '$func$2$f$',
connectionLabel: '$func$2$f$',
},
{
type: 'custom',
test: '$func$2$f$',
connectionLabel: { url: 'abc' },
},
],
antiExamples: [
{
example: {},
reason: 'Missing required keys: type and test',
},
{
example: '$func$2$f$',
reason: 'Must be object',
},
{
example: {
type: 'unknown',
test: '$func$2$f$',
},
reason: 'Invalid value for key: type',
},
{
example: {
type: 'custom',
test: '$func$2$f$',
fields: '$func$2$f$',
},
reason: 'Invalid value for key: fields',
},
],
},
[
FunctionSchema,
RequestSchema,
AuthFieldsSchema,
AuthenticationBasicConfigSchema,
AuthenticationCustomConfigSchema,
AuthenticationDigestConfigSchema,
AuthenticationOAuth1ConfigSchema,
AuthenticationOAuth2ConfigSchema,
AuthenticationSessionConfigSchema,
],
);

View file

@ -0,0 +1,31 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const RequestSchema = require('./RequestSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/AuthenticationSessionConfigSchema',
description: 'Config for session authentication.',
type: 'object',
required: ['perform'],
properties: {
perform: {
description:
'Define how Zapier fetches the additional authData needed to make API calls.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
},
additionalProperties: false,
examples: [{ perform: { require: 'some/path/to/file.js' } }],
antiExamples: [
{
example: {},
reason: 'Missing required key: perform',
},
],
},
[FunctionSchema, RequestSchema],
);

View file

@ -0,0 +1,63 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const BasicOperationSchema = require('./BasicOperationSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicActionOperationSchema = JSON.parse(
JSON.stringify(BasicOperationSchema.schema),
);
BasicActionOperationSchema.id = '/BasicActionOperationSchema';
BasicActionOperationSchema.description =
'Represents the fundamental mechanics of a search/create.';
BasicActionOperationSchema.properties = {
resource: BasicActionOperationSchema.properties.resource,
perform: BasicActionOperationSchema.properties.perform,
performResume: {
description:
'A function that parses data from a perform (which uses z.generateCallbackUrl()) and callback request to resume this action.',
$ref: FunctionSchema.id,
},
performGet: {
description:
'How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
inputFields: BasicActionOperationSchema.properties.inputFields,
inputFieldGroups: BasicActionOperationSchema.properties.inputFieldGroups,
outputFields: BasicActionOperationSchema.properties.outputFields,
sample: BasicActionOperationSchema.properties.sample,
lock: BasicActionOperationSchema.properties.lock,
throttle: BasicActionOperationSchema.properties.throttle,
cleanInputData: BasicActionOperationSchema.properties.cleanInputData,
};
BasicActionOperationSchema.examples = [
{
perform: { require: 'some/path/to/file.js' },
sample: { id: 42, name: 'Hooli' },
},
];
BasicActionOperationSchema.antiExamples = [
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true / top-level resource doesn't have sample
example: {
perform: { require: 'some/path/to/file.js' },
},
reason:
'Missing required key: sample. Note - This is only invalid if `display` is not explicitly set to true and if it does not belong to a resource that has a sample.',
},
];
module.exports = makeSchema(
BasicActionOperationSchema,
BasicOperationSchema.dependencies,
);

View file

@ -0,0 +1,61 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
const BufferConfigSchema = require('./BufferConfigSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicCreateOperationSchema = JSON.parse(
JSON.stringify(BasicActionOperationSchema.schema),
);
BasicCreateOperationSchema.id = '/BasicCreateOperationSchema';
BasicCreateOperationSchema.description =
'Represents the fundamental mechanics of a create.';
BasicCreateOperationSchema.properties.perform = {
description:
"How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`. Exactly one of `perform` or `performBuffer` must be defined. If you choose to define `buffer` and `performBuffer`, you must omit `perform`.",
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
docAnnotation: {
required: {
type: 'replace', // replace or append
value: 'no (with exceptions, see description)',
},
},
};
BasicCreateOperationSchema.properties.buffer = {
description:
'Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk with `performBuffer`.',
$ref: BufferConfigSchema.id,
docAnnotation: {
required: {
type: 'replace', // replace or append
value: 'no (with exceptions, see description)',
},
},
};
BasicCreateOperationSchema.properties.performBuffer = {
description:
'Currently an **internal-only** feature. A function to create objects in bulk with. `buffer` and `performBuffer` must either both be defined or neither. Additionally, only one of `perform` or `performBuffer` can be defined. If you choose to define `perform`, you must omit `buffer` and `performBuffer`.',
$ref: FunctionSchema.id,
docAnnotation: {
required: {
type: 'replace', // replace or append
value: 'no (with exceptions, see description)',
},
},
};
delete BasicCreateOperationSchema.required;
module.exports = makeSchema(
BasicCreateOperationSchema,
BasicActionOperationSchema.dependencies.concat(BufferConfigSchema),
);

View file

@ -0,0 +1,78 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
module.exports = makeSchema({
id: '/BasicDisplaySchema',
description: 'Represents user information for a trigger, search, or create.',
type: 'object',
properties: {
label: {
description:
'A short label like "New Record" or "Create Record in Project". Optional if `hidden` is true.',
type: 'string',
minLength: 2,
maxLength: 64,
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
description: {
description:
'A description of what this trigger, search, or create does. Optional if `hidden` is true.',
type: 'string',
minLength: 1,
maxLength: 1000,
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
directions: {
description:
'A short blurb that can explain how to get this working. EG: how and where to copy-paste a static hook URL into your application. Only evaluated for static webhooks.',
type: 'string',
minLength: 12,
maxLength: 1000,
},
hidden: {
description: 'Should this operation be unselectable by users?',
type: 'boolean',
},
},
additionalProperties: false,
examples: [
{ hidden: true },
{ label: 'New Thing', description: 'Gets a new thing for you.' },
{
label: 'New Thing',
description: 'Gets a new thing for you.',
directions: 'This is how you use the thing.',
hidden: false,
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that description is required if hidden is false
example: {
label: 'New Thing',
hidden: false,
},
reason: 'Missing required key: description',
},
{
[SKIP_KEY]: true, // Cannot validate that description is required if hidden is false
example: {
description: 'Gets a new thing for you.',
hidden: false,
},
reason: 'Missing required key: label',
},
],
});

View file

@ -0,0 +1,119 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const BasicOperationSchema = require('./BasicOperationSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicHookOperationSchema = JSON.parse(
JSON.stringify(BasicOperationSchema.schema),
);
const hookTechnicallyRequired =
'Note: this is required for public apps to ensure the best UX for the end-user. For private apps, this is strongly recommended for testing REST Hooks. Otherwise, you can ignore warnings about this property with the `--without-style` flag during `zapier-platform push`.';
BasicHookOperationSchema.id = '/BasicHookOperationSchema';
BasicHookOperationSchema.description =
'Represents the inbound mechanics of hooks with optional subscribe/unsubscribe. Defers to list for fields.';
BasicHookOperationSchema.properties = {
type: {
description:
'Must be explicitly set to `"hook"` unless this hook is defined as part of a resource, in which case it\'s optional.',
type: 'string',
enum: ['hook'],
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
resource: BasicHookOperationSchema.properties.resource,
perform: {
description: 'A function that processes the inbound webhook request.',
$ref: FunctionSchema.id,
},
performList: {
description:
'Fetch a list of items on demand during testing instead of waiting for a hook. You can also consider resources and their built-in hook/list methods. ' +
hookTechnicallyRequired,
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
canPaginate: {
description:
'Does this endpoint support pagination via temporary cursor storage?',
type: 'boolean',
},
performSubscribe: {
description:
'Takes a URL and any necessary data from the user and subscribes. ' +
hookTechnicallyRequired,
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
performUnsubscribe: {
description:
'Takes a URL and data from a previous subscribe call and unsubscribes. ' +
hookTechnicallyRequired,
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
docAnnotation: {
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
},
inputFields: BasicHookOperationSchema.properties.inputFields,
inputFieldGroups: BasicHookOperationSchema.properties.inputFieldGroups,
outputFields: BasicHookOperationSchema.properties.outputFields,
sample: BasicHookOperationSchema.properties.sample,
cleanInputData: BasicHookOperationSchema.properties.cleanInputData,
};
BasicHookOperationSchema.examples = [
{
type: 'hook',
perform: { require: 'some/path/to/file.js' },
performList: { require: 'some/path/to/file2.js' },
performSubscribe: { require: 'some/path/to/file3.js' },
performUnsubscribe: { require: 'some/path/to/file4.js' },
sample: { id: 42, name: 'Hooli' },
},
];
BasicHookOperationSchema.antiExamples = [
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true / top-level resource doesn't have sample
example: {
type: 'hook',
perform: { require: 'some/path/to/file.js' },
performList: { require: 'some/path/to/file2.js' },
performSubscribe: { require: 'some/path/to/file3.js' },
performUnsubscribe: { require: 'some/path/to/file4.js' },
},
reason:
'Missing required key: sample. Note - This is only invalid if `display` is not explicitly set to true and if it does not belong to a resource that has a sample.',
},
];
module.exports = makeSchema(
BasicHookOperationSchema,
BasicOperationSchema.dependencies,
);

View file

@ -0,0 +1,100 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const BasicOperationSchema = require('./BasicOperationSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicHookToPollOperationSchema = JSON.parse(
JSON.stringify(BasicOperationSchema.schema),
);
BasicHookToPollOperationSchema.id = '/BasicHookToPollOperationSchema';
BasicHookToPollOperationSchema.description =
'Represents the inbound mechanics of hook to poll style triggers. Defers to list for fields.';
BasicHookToPollOperationSchema.docAnnotation = {
hide: true,
};
BasicHookToPollOperationSchema.required = [
'performList',
'performSubscribe',
'performUnsubscribe',
];
BasicHookToPollOperationSchema.properties = {
type: {
description: 'Must be explicitly set to `"hook_to_poll"`.',
type: 'string',
enum: ['hook_to_poll'],
required: {
type: 'replace',
value: '**yes** (with exceptions, see description)',
},
},
performList: {
description:
'Similar a polling trigger, but checks for new data when a webhook is received, instead of every few minutes',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
canPaginate: {
description:
'Does this endpoint support pagination via temporary cursor storage?',
type: 'boolean',
},
performSubscribe: {
description:
'Takes a URL and any necessary data from the user and subscribes. ',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
performUnsubscribe: {
description:
'Takes a URL and data from a previous subscribe call and unsubscribes. ',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
inputFields: BasicHookToPollOperationSchema.properties.inputFields,
inputFieldGroups: BasicHookToPollOperationSchema.properties.inputFieldGroups,
outputFields: BasicHookToPollOperationSchema.properties.outputFields,
sample: BasicHookToPollOperationSchema.properties.sample,
cleanInputData: BasicHookToPollOperationSchema.properties.cleanInputData,
maxPollingDelay: {
description:
'The maximum amount of time to wait between polling requests in seconds. Minimum value is 20s and will default to 20 if not set, or set to a lower value.',
type: 'integer',
},
};
BasicHookToPollOperationSchema.examples = [
{
type: 'hook_to_poll',
performList: { require: 'some/path/to/file2.js' },
performSubscribe: { require: 'some/path/to/file3.js' },
performUnsubscribe: { require: 'some/path/to/file4.js' },
sample: { id: 42, name: 'Hooli' },
},
];
BasicHookToPollOperationSchema.antiExamples = [
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true
example: {
type: 'hook_to_poll',
performList: { require: 'some/path/to/file2.js' },
performSubscribe: { require: 'some/path/to/file3.js' },
performUnsubscribe: { require: 'some/path/to/file4.js' },
},
reason:
'Missing required key: sample. Note - This is only invalid if `display` is not explicitly set to true',
},
];
module.exports = makeSchema(
BasicHookToPollOperationSchema,
BasicOperationSchema.dependencies,
);

View file

@ -0,0 +1,107 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
const ResultsSchema = require('./ResultsSchema');
const KeySchema = require('./KeySchema');
const LockObjectSchema = require('./LockObjectSchema');
const ThrottleObjectSchema = require('./ThrottleObjectSchema');
const InputFieldsSchema = require('./InputFieldsSchema');
const InputFieldGroupsSchema = require('./InputFieldGroupsSchema');
const OutputFieldsSchema = require('./OutputFieldsSchema');
module.exports = makeSchema(
{
id: '/BasicOperationSchema',
description:
'Represents the fundamental mechanics of triggers, searches, or creates.',
type: 'object',
required: ['perform'],
properties: {
resource: {
description:
'Optionally reference and extends a resource. Allows Zapier to automatically tie together samples, lists and hooks, greatly improving the UX. EG: if you had another trigger reusing a resource but filtering the results.',
$ref: KeySchema.id,
},
perform: {
description:
"How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.",
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
inputFields: {
description:
'What should the form a user sees and configures look like?',
$ref: InputFieldsSchema.id,
},
inputFieldGroups: {
description:
'Defines groups for organizing input fields in the UI. Each group can have a key, label, and emphasis styling.',
$ref: InputFieldGroupsSchema.id,
},
outputFields: {
description:
'What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.',
$ref: OutputFieldsSchema.id,
},
sample: {
description:
'What does a sample of data look like? Will use resource sample if missing. Requirement waived if `display.hidden` is true or if this belongs to a resource that has a top-level sample',
type: 'object',
// TODO: require id, ID, Id property?
minProperties: 1,
docAnnotation: {
required: {
type: 'replace', // replace or append
value: '**yes** (with exceptions, see description)',
},
},
},
lock: {
description:
'Zapier uses this configuration to ensure this action is performed one at a time per scope (avoid concurrency).',
$ref: LockObjectSchema.id,
},
throttle: {
description:
'Zapier uses this configuration to apply throttling when the limit for the window is exceeded.',
$ref: ThrottleObjectSchema.id,
},
cleanInputData: {
description:
'If true, Zapier removes empty strings, `null`, `undefined`, and empty Arrays or objects from `bundle.inputData` recursively before passing it to your `perform*` function. If you want to handle empty values yourself in your code, explicitly set this to false. There is also a global flag with the same name in `App.flags`. This one takes precedence over the global one.',
type: 'boolean',
},
},
examples: [
{
perform: { require: 'some/path/to/file.js' },
sample: { id: 42, name: 'Hooli' },
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true / top-level resource doesn't have sample
example: {
perform: { require: 'some/path/to/file.js' },
},
reason:
'Missing required key: sample. Note - This is only invalid if `display` is not explicitly set to true and if it does not belong to a resource that has a sample.',
},
],
additionalProperties: false,
},
[
InputFieldsSchema,
InputFieldGroupsSchema,
OutputFieldsSchema,
FunctionSchema,
KeySchema,
LockObjectSchema,
RequestSchema,
ResultsSchema,
ThrottleObjectSchema,
],
);

View file

@ -0,0 +1,44 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicOperationSchema = require('./BasicOperationSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicPollingOperationSchema = JSON.parse(
JSON.stringify(BasicOperationSchema.schema),
);
BasicPollingOperationSchema.id = '/BasicPollingOperationSchema';
BasicPollingOperationSchema.description =
'Represents the fundamental mechanics of a trigger.';
BasicPollingOperationSchema.properties = {
type: {
// TODO: not a fan of this...
description:
'Clarify how this operation works (polling == pull or hook == push).',
type: 'string',
default: 'polling',
enum: ['polling'], // notification?
},
resource: BasicPollingOperationSchema.properties.resource,
perform: BasicPollingOperationSchema.properties.perform,
canPaginate: {
description:
'Does this endpoint support pagination via temporary cursor storage?',
type: 'boolean',
},
inputFields: BasicPollingOperationSchema.properties.inputFields,
inputFieldGroups: BasicPollingOperationSchema.properties.inputFieldGroups,
outputFields: BasicPollingOperationSchema.properties.outputFields,
sample: BasicPollingOperationSchema.properties.sample,
throttle: BasicPollingOperationSchema.properties.throttle,
cleanInputData: BasicPollingOperationSchema.properties.cleanInputData,
};
module.exports = makeSchema(
BasicPollingOperationSchema,
BasicOperationSchema.dependencies,
);

View file

@ -0,0 +1,67 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const BasicOperationSchema = require('./BasicOperationSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
// TODO: would be nice to deep merge these instead
// or maybe use allOf which is built into json-schema
const BasicSearchOperationSchema = JSON.parse(
JSON.stringify(BasicOperationSchema.schema),
);
BasicSearchOperationSchema.id = '/BasicSearchOperationSchema';
BasicSearchOperationSchema.description =
'Represents the fundamental mechanics of a search.';
BasicSearchOperationSchema.properties = {
resource: BasicSearchOperationSchema.properties.resource,
perform: BasicSearchOperationSchema.properties.perform,
performResume: {
description:
'A function that parses data from a perform (which uses z.generateCallbackUrl()) and callback request to resume this action.',
$ref: FunctionSchema.id,
},
performGet: {
description:
'How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.',
oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }],
},
canPaginate: {
description: 'Does this search support pagination?',
type: 'boolean',
},
inputFields: BasicSearchOperationSchema.properties.inputFields,
inputFieldGroups: BasicSearchOperationSchema.properties.inputFieldGroups,
outputFields: BasicSearchOperationSchema.properties.outputFields,
sample: BasicSearchOperationSchema.properties.sample,
lock: BasicSearchOperationSchema.properties.lock,
throttle: BasicSearchOperationSchema.properties.throttle,
cleanInputData: BasicSearchOperationSchema.properties.cleanInputData,
};
BasicSearchOperationSchema.examples = [
{
perform: { require: 'some/path/to/file.js' },
sample: { id: 42, name: 'Hooli' },
},
];
BasicSearchOperationSchema.antiExamples = [
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true / top-level resource doesn't have sample
example: {
perform: { require: 'some/path/to/file.js' },
},
reason:
'Missing required key: sample. Note - This is only invalid if `display` is not explicitly set to true and if it does not belong to a resource that has a sample.',
},
];
module.exports = makeSchema(
BasicSearchOperationSchema,
BasicOperationSchema.dependencies,
);

View file

@ -0,0 +1,48 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/BufferConfigSchema',
description:
'Currently an **internal-only** feature. Zapier uses this configuration for creating objects in bulk.',
type: 'object',
required: ['groupedBy', 'limit'],
properties: {
groupedBy: {
description:
'The list of keys of input fields to group bulk-create with. The actual user data provided for the fields will be used during execution. Note that a required input field should be referenced to get user data always.',
type: 'array',
minItems: 1,
},
limit: {
description:
"The maximum number of items to call `performBuffer` with. **Note** that it is capped by the platform to prevent exceeding the [AWS Lambda's request/response payload size quota of 6 MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution). Also, the execution is time-bound; we recommend reducing it upon consistent timeout.",
type: 'integer',
},
},
examples: [
{
groupedBy: ['workspace', 'sheet'],
limit: 100,
},
],
antiExamples: [
{
example: {
groupedBy: [],
limit: 100,
},
reason: 'Empty groupedBy list provided: `[]`.',
},
{
example: { groupedBy: ['workspace'] },
reason: 'Missing required key: `limit`.',
},
{
example: { limit: 1 },
reason: 'Missing required key: `groupedBy`.',
},
],
additionalProperties: false,
});

View file

@ -0,0 +1,72 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/BulkReadSchema',
description: 'How will Zapier fetch resources from your application?',
type: 'object',
required: ['key', 'noun', 'display', 'operation'],
properties: {
key: {
description: 'A key to uniquely identify a record.',
$ref: KeySchema.id,
},
noun: {
description:
'A noun for this read that completes the sentence "reads all of the XXX".',
type: 'string',
minLength: 2,
maxLength: 255,
},
display: {
description: 'Configures the UI for this read bulk.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Powers the functionality for this read bulk.',
$ref: BasicActionOperationSchema.id,
},
},
examples: [
{
key: 'recipes',
noun: 'Recipes',
display: {
label: 'Recipes',
description: 'A Read that lets Zapier fetch all recipes.',
},
operation: {
perform: '$func$0$f$',
sample: {
id: 1,
firstName: 'Walter',
lastName: 'Sobchak',
occupation: 'Bowler',
},
},
},
],
antiExamples: [
{
example: {
display: {
label: 'Get User',
description: 'Retrieve a user.',
},
operation: {
description: 'Define how this search method will work.',
},
},
reason: 'Missing required keys: key and noun',
},
],
additionalProperties: false,
},
[KeySchema, BasicDisplaySchema, BasicActionOperationSchema],
);

View file

@ -0,0 +1,69 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const BulkReadSchema = require('./BulkReadSchema');
module.exports = makeSchema(
{
id: '/BulkReadsSchema',
description: 'Enumerates the bulk reads your app exposes.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the BulkReadSchema.',
$ref: BulkReadSchema.id,
},
},
additionalProperties: false,
examples: [
{
recipes: {
key: 'recipes',
noun: 'Recipes',
display: {
label: 'Recipes',
description: 'A Read that lets Zapier fetch all recipes.',
},
operation: {
perform: '$func$0$f$',
sample: {
id: 1,
firstName: 'Walter',
lastName: 'Sobchak',
occupation: 'Bowler',
},
},
},
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that keys don't match
example: {
readRecipes: {
key: 'recipes',
noun: 'Recipes',
display: {
label: 'Recipes',
description: 'A Read that lets Zapier fetch all recipes.',
},
operation: {
perform: '$func$0$f$',
sample: {
id: 1,
firstName: 'Walter',
lastName: 'Sobchak',
occupation: 'Bowler',
},
},
},
},
reason: 'Key must match the key of the associated BulkReadSchema',
},
],
},
[BulkReadSchema],
);

View file

@ -0,0 +1,17 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/BundleSchema',
description: 'Given as the "arguments" or input to a perform call.',
type: 'object',
examples: [{}, { authData: {}, inputData: {}, inputDataRaw: {} }],
antiExamples: [{ random: true }],
properties: {
authData: { type: 'object' },
inputData: { type: 'object' },
inputDataRaw: { type: 'object' },
},
additionalProperties: false,
});

View file

@ -0,0 +1,83 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicCreateOperationSchema = require('./BasicCreateOperationSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/CreateSchema',
description: 'How will Zapier create a new object?',
type: 'object',
required: ['key', 'noun', 'display', 'operation'],
properties: {
key: {
description: 'A key to uniquely identify this create.',
$ref: KeySchema.id,
},
noun: {
description:
'A noun for this create that completes the sentence "creates a new XXX".',
type: 'string',
minLength: 2,
maxLength: 255,
},
display: {
description: 'Configures the UI for this create.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Powers the functionality for this create.',
$ref: BasicCreateOperationSchema.id,
},
},
examples: [
{
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: { perform: '$func$2$f$', sample: { id: 1 } },
},
{
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
hidden: true,
},
operation: {
perform: '$func$2$f$',
},
},
],
antiExamples: [
{
example: 'abc',
reason: 'Must be an object',
},
{
example: {
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: {
perform: '$func$2$f$',
},
},
reason:
'Missing required key on operation: sample. Note - this is valid if the resource has defined a sample.',
},
],
additionalProperties: false,
},
[BasicDisplaySchema, BasicCreateOperationSchema, KeySchema],
);

View file

@ -0,0 +1,79 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const CreateSchema = require('./CreateSchema');
module.exports = makeSchema(
{
id: '/CreatesSchema',
description: 'Enumerates the creates your app has available for users.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the CreateSchema.',
$ref: CreateSchema.id,
},
},
additionalProperties: false,
examples: [
{
createRecipe: {
key: 'createRecipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: { perform: '$func$2$f$', sample: { id: 1 } },
},
},
{
Create_Recipe_01: {
key: 'Create_Recipe_01',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: { perform: '$func$2$f$', sample: { id: 1 } },
},
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that key matches pattern
example: {
'01_Create_Recipe': {
key: '01_Create_Recipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: { perform: '$func$2$f$', sample: { id: 1 } },
},
},
reason: 'Key must start with a letter',
},
{
[SKIP_KEY]: true, // Cannot validate that keys match
example: {
Create_Recipe: {
key: 'createRecipe',
noun: 'Recipe',
display: {
label: 'Create Recipe',
description: 'Creates a new recipe.',
},
operation: { perform: '$func$2$f$', sample: { id: 1 } },
},
},
reason: 'Key must match the key field in CreateSchema',
},
],
},
[CreateSchema],
);

View file

@ -0,0 +1,37 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/FieldChoiceWithLabelSchema',
description:
"An object describing a labeled choice in a static dropdown. Useful if the value a user picks isn't exactly what the zap uses. For instance, when they click on a nickname, but the zap uses the user's full name ([image](https://cdn.zapier.com/storage/photos/8ed01ac5df3a511ce93ed2dc43c7fbbc.png)).",
type: 'object',
required: ['value', 'sample', 'label'],
properties: {
value: {
description:
'The actual value that is sent into the Zap. This is displayed as light grey text in the editor. Should match sample exactly.',
type: 'string',
minLength: 1,
},
sample: {
description:
'A legacy field that is no longer used by the editor, but it is still required for now and should match the value.',
type: 'string',
minLength: 1,
},
label: {
description: 'A human readable label for this value.',
type: 'string',
minLength: 1,
},
},
examples: [{ label: 'Red', sample: '#f00', value: '#f00' }],
antiExamples: [
{
example: { label: 'Red', value: '#f00' },
reason: 'Missing required key: sample',
},
],
});

View file

@ -0,0 +1,41 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FieldChoiceWithLabelSchema = require('./FieldChoiceWithLabelSchema');
module.exports = makeSchema(
{
id: '/FieldChoicesSchema',
description:
'A static dropdown of options. Which you use depends on your order and label requirements:\n\nNeed a Label? | Does Order Matter? | Type to Use\n---|---|---\nYes | No | Object of value -> label\nNo | Yes | Array of Strings\nYes | Yes | Array of [FieldChoiceWithLabel](#fieldchoicewithlabelschema)',
oneOf: [
{
type: 'object',
minProperties: 1,
not: { required: ['perform'] },
},
{
type: 'array',
minItems: 1,
items: {
oneOf: [{ type: 'string' }, { $ref: FieldChoiceWithLabelSchema.id }],
},
},
],
examples: [{ a: '1', b: '2', c: '3' }, ['first', 'second', 'third']],
antiExamples: [
{
example: [1, 2, 3],
reason:
'If an array, must be of either type string or FieldChoiceWithLabelSchema',
},
{
example: [{ a: '1', b: '2', c: '3' }],
reason:
'If an array, must be of either type string or FieldChoiceWithLabelSchema',
},
],
},
[FieldChoiceWithLabelSchema],
);

View file

@ -0,0 +1,54 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
const RequestSchema = require('./RequestSchema');
module.exports = makeSchema(
{
id: '/FieldDynamicChoicesSchema',
description:
'Describes dynamic dropdowns powered by a perform function or request.',
type: 'object',
required: ['perform'],
properties: {
perform: {
description:
'A function or request that returns choices for this dynamic dropdown.',
oneOf: [{ $ref: FunctionSchema.id }, { $ref: RequestSchema.id }],
},
},
additionalProperties: false,
examples: [
{ perform: '$func$0$f$' },
{ perform: { source: 'return []' } },
{
perform: {
method: 'GET',
url: 'https://api.example.com/choices',
},
},
],
antiExamples: [
{
example: {},
reason: 'Missing required key: perform',
},
{
example: { someKey: 'value' },
reason: 'Missing required key: perform',
},
{
example: { perform: 'invalid' },
reason:
'Invalid value for key: perform (must be a function or request)',
},
{
example: { perform: '$func$0$f$', unknownKey: 'value' },
reason: 'Invalid extra property: unknownKey',
},
],
},
[FunctionSchema, RequestSchema],
);

View file

@ -0,0 +1,29 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/FieldMetaSchema',
type: 'object',
description: 'Allows for additional metadata to be stored on the field.',
patternProperties: {
'[^\\s]+': {
description: 'Only string, integer or boolean values are allowed.',
anyOf: [{ type: 'string' }, { type: 'integer' }, { type: 'boolean' }],
},
},
examples: [
{ shouldCapitalize: true },
{ shouldCapitalize: true, internalType: 'datetime' },
],
antiExamples: [
{
example: { databank: { primaryContact: 'abc' } },
reason: 'No complex values allowed',
},
{
example: { needsProcessing: null },
reason: 'No null values allowed',
},
],
});

View file

@ -0,0 +1,45 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/FlatObjectSchema',
description: 'An object whose values can only be primitives',
type: 'object',
patternProperties: {
'[^\\s]+': {
description:
'Any key may exist in this flat object as long as its values are simple.',
anyOf: [
{ type: 'null' },
{ type: 'string' },
{ type: 'integer' },
{ type: 'number' },
{ type: 'boolean' },
],
},
},
examples: [
{ a: 1, b: 2, c: 3 },
{ a: 1.2, b: 2.2, c: 3.3 },
{ a: 'a', b: 'b', c: 'c' },
{ a: true, b: true, c: false },
{ a: 'a', b: 2, c: 3.1, d: true, e: false },
{ 123: 'hello' },
],
antiExamples: [
{
example: { a: {}, b: 2 },
reason: 'Invalid value for key: a (objects are not allowed)',
},
{
example: { a: [], b: 2 },
reason: 'Invalid value for key: a (arrays are not allowed)',
},
{
example: { '': 1 },
reason: 'Key cannot be empty',
},
],
additionalProperties: false,
});

View file

@ -0,0 +1,28 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/FunctionRequireSchema',
description:
'A path to a file that might have content like `module.exports = (z, bundle) => [{id: 123}];`.',
type: 'object',
required: ['require'],
properties: {
require: { type: 'string' },
},
additionalProperties: false,
examples: [{ require: 'some/path/to/file.js' }],
antiExamples: [
{
example: {},
reason: 'Missing required key: require',
},
{
example: {
required: 2,
},
reason: 'Invalid value for key: required (must be of type string)',
},
],
});

View file

@ -0,0 +1,42 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionRequireSchema = require('./FunctionRequireSchema');
const FunctionSourceSchema = require('./FunctionSourceSchema');
module.exports = makeSchema(
{
id: '/FunctionSchema',
description:
'Internal pointer to a function from the original source or the source code itself. Encodes arity and if `arguments` is used in the body. Note - just write normal functions and the system will encode the pointers for you. Or, provide {source: "return 1 + 2"} and the system will wrap in a function for you.',
oneOf: [
{ type: 'string', pattern: '^\\$func\\$\\d+\\$[tf]\\$$' },
{ $ref: FunctionRequireSchema.id },
{ $ref: FunctionSourceSchema.id },
],
examples: [
'$func$0$f$',
'$func$2$t$',
{ source: 'return 1 + 2' },
{ require: 'some/path/to/file.js' },
],
antiExamples: [
{
example: 'funcy',
reason: 'Invalid function reference',
},
{
example: { source: '1 + 2' },
reason:
'Invalid value for key: source (must end with a `return` statement)',
},
{
example: { source: '1 + 2', require: 'some/path/to/file.js' },
reason:
'Must be either /FunctionRequireSchema _or_ /FunctionSourceSchema',
},
],
},
[FunctionRequireSchema, FunctionSourceSchema],
);

View file

@ -0,0 +1,37 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/FunctionSourceSchema',
description:
'Source code like `{source: "return 1 + 2"}` which the system will wrap in a function for you.',
type: 'object',
required: ['source'],
properties: {
source: {
type: 'string',
pattern: 'return',
description:
'JavaScript code for the function body. This must end with a `return` statement.',
},
args: {
type: 'array',
items: { type: 'string' },
description:
"Function signature. Defaults to `['z', 'bundle']` if not specified.",
},
},
additionalProperties: false,
examples: [
{ source: 'return 1 + 2' },
{ args: ['x', 'y'], source: 'return x + y;' },
],
antiExamples: [
{
example: { source: '1 + 2' },
reason:
'Invalid value for key: source (must end with a `return` statement)',
},
],
});

View file

@ -0,0 +1,29 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/HydratorsSchema',
description:
"A bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.",
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9]*$': {
description:
"Any unique key can be used in `z.hydrate('uniqueKeyHere')`.",
$ref: FunctionSchema.id,
},
},
additionalProperties: false,
examples: [{ hydrateFile: { require: 'some/path/to/file.js' } }],
antiExamples: [
{
example: { '12th': { require: 'some/path/to/file.js' } },
reason: 'Invalid key (must start with a letter)',
},
],
},
[FunctionSchema],
);

View file

@ -0,0 +1,39 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/InputFieldGroupSchema',
description: 'Object for visual grouping of input fields.',
type: 'object',
required: ['key'],
properties: {
key: {
description: 'The unique identifier for this group.',
$ref: KeySchema.id,
},
label: {
description: 'The human-readable name for the group.',
type: 'string',
minLength: 1,
},
emphasize: {
description:
'Whether this group should be visually emphasized in the UI.',
type: 'boolean',
},
},
examples: [
{ key: 'testGroup' },
{ key: 'testGroup', label: 'Test Group', emphasize: true },
],
antiExamples: [
{ example: { label: 'test label' }, reason: 'key is required' },
],
additionalProperties: false,
},
[KeySchema],
);

View file

@ -0,0 +1,23 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const InputFieldGroupSchema = require('./InputFieldGroupSchema');
module.exports = makeSchema(
{
id: '/InputFieldGroupsSchema',
description: 'An array or collection of input field groups.',
type: 'array',
items: {
$ref: InputFieldGroupSchema.id,
},
examples: [[{ key: 'abc' }]],
antiExamples: [
{ example: {}, reason: 'Must be an array' },
{ example: [{ label: 'test label' }], reason: 'key is required' },
],
additionalProperties: false,
},
[InputFieldGroupSchema],
);

View file

@ -0,0 +1,20 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const PlainInputFieldSchema = require('./PlainInputFieldSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/InputFieldsSchema',
description: 'An array or collection of input fields.',
type: 'array',
items: {
oneOf: [{ $ref: PlainInputFieldSchema.id }, { $ref: FunctionSchema.id }],
},
examples: [[{ key: 'abc' }]],
antiExamples: [{ example: {}, reason: 'Must be an array' }],
},
[PlainInputFieldSchema, FunctionSchema],
);

View file

@ -0,0 +1,47 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/JsonSchemaSchema',
description:
'A JSON Schema object that describes the expected structure of a JSON value. Validated against JSON Schema Draft 4, 6, or 7 meta-schema (based on the `$schema` field, defaulting to Draft 7) via the validateJsonFieldSchema functional constraint.',
type: 'object',
additionalProperties: true,
examples: [
{ type: 'object', properties: { name: { type: 'string' } } },
{ type: 'array', items: { type: 'string' } },
{},
{
allOf: [{ type: 'object' }, { properties: { name: { type: 'string' } } }],
not: { type: 'array' },
},
{
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
},
required: ['name'],
additionalProperties: false,
},
{
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
name: { type: 'string' },
},
},
],
antiExamples: [
{
example: ['not', 'an', 'object'],
reason: 'JSON Schema must be an object, not an array',
},
{
example: { type: 'string' },
reason:
"JSON Schema type should be an object or an array. If a primitive is needed, use `type: 'string'` on the input field directly",
},
],
});

View file

@ -0,0 +1,30 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/KeySchema',
description: 'A unique identifier for this item.',
type: 'string',
minLength: 2,
pattern: '^[a-zA-Z]+[a-zA-Z0-9_]*$',
examples: ['vk', 'validKey', 'ValidKey', 'Valid_Key_2'],
antiExamples: [
{
example: '',
reason: 'Cannot be empty',
},
{
example: 'A',
reason: 'Minimum of two characters',
},
{
example: '1_Key',
reason: 'Must start with a letter',
},
{
example: 'a-Key',
reason: 'Must not use dashes',
},
],
});

View file

@ -0,0 +1,56 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/LockObjectSchema',
description:
'Zapier uses this configuration to ensure this action is performed one at a time per scope (avoid concurrency).',
type: 'object',
required: ['key'],
properties: {
key: {
description:
'The key to use for locking. This should be unique to the operation. While actions of different integrations with the same key and scope will never lock each other out, actions of the same integration with the same key and scope will do. User data provided for the input fields can be used in the key with the use of the curly braces referencing. For example, to access the user data provided for the input field "test_field", use `{{bundle.inputData.test_field}}`. Note that a required input field should be referenced to get user data always.',
type: 'string',
minLength: 1,
},
scope: {
description: `By default, locks are scoped to the app. That is, all users of the app will share the same locks. If you want to restrict serial access to a specific user, auth, or account, you can set the scope to one or more of the following: 'user' - Locks based on user ids. 'auth' - Locks based on unique auth ids. 'account' - Locks for all users under a single account. You may also combine scopes. Note that "app" is included, always, in the scope provided. For example, a scope of ['account', 'auth'] would result to ['app', 'account', 'auth'].`,
type: 'array',
items: {
enum: ['user', 'auth', 'account'],
type: 'string',
},
},
timeout: {
description:
'The number of seconds to hold the lock before releasing it to become accessible to other task invokes that need it. If not provided, the default set by the app will be used. It cannot be more than 180.',
type: 'integer',
},
},
examples: [
{
key: 'random_key',
scope: ['account', 'user'],
timeout: 30,
},
{
key: '{{bundle.inputData.test_field}}',
},
],
antiExamples: [
{
example: {
key: 'random_key',
scope: ['zap'],
},
reason: 'Invalid scope provided: `zap`.',
},
{
example: {},
reason: 'Missing required key: `key`.',
},
],
additionalProperties: false,
});

View file

@ -0,0 +1,35 @@
'use strict';
const { SKIP_KEY } = require('../constants');
const makeSchema = require('../utils/makeSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/MiddlewaresSchema',
description:
'List of before or after middlewares. Can be an array of functions or a single function',
oneOf: [
{
type: 'array',
items: { $ref: FunctionSchema.id },
},
{ $ref: FunctionSchema.id },
],
additionalProperties: false,
examples: [
{
[SKIP_KEY]: true, // TODO fix this
require: 'some/path/to/file.js',
},
[{ require: 'some/path/to/file.js' }],
],
antiExamples: [
{
example: {},
reason: 'Does not match either /FunctionSchema or an array of such',
},
],
},
[FunctionSchema],
);

View file

@ -0,0 +1,20 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const PlainOutputFieldSchema = require('./PlainOutputFieldSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/OutputFieldsSchema',
description: 'An array or collection of output fields.',
type: 'array',
items: {
oneOf: [{ $ref: PlainOutputFieldSchema.id }, { $ref: FunctionSchema.id }],
},
examples: [[{ key: 'abc' }]],
antiExamples: [{ example: {}, reason: 'Must be an array' }],
},
[PlainOutputFieldSchema, FunctionSchema],
);

View file

@ -0,0 +1,148 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { INCOMPATIBLE_FIELD_SCHEMA_KEYS } = require('../constants');
// the following takes an array of string arrays (string[][]) and returns the follwing string:
// * `a` & `b`
// * `c` & `d`
// ... etc
const wrapInBackticks = (s) => `\`${s}\``;
const formatBullet = (f) => `* ${f.map(wrapInBackticks).join(' & ')}`;
const incompatibleFieldsList =
INCOMPATIBLE_FIELD_SCHEMA_KEYS.map(formatBullet).join('\n');
module.exports = makeSchema({
id: '/PlainFieldSchema',
description: `In addition to the requirements below, the following keys are mutually exclusive:\n\n${incompatibleFieldsList}`,
type: 'object',
required: ['key'],
docAnnotation: { hide: true },
properties: {
key: {
description:
'A unique machine readable key for this value (IE: "fname").',
type: 'string',
minLength: 1,
},
label: {
description: 'A human readable label for this value (IE: "First Name").',
type: 'string',
minLength: 1,
},
type: {
description:
'The type of this value. Use `string` for basic text input, `text` for a large, `<textarea>` style box, and `code` for a `<textarea>` with a fixed-width font. Field type of `file` will accept either a file object or a string. If a URL is provided in the string, Zapier will automatically make a GET for that file. Otherwise, a .txt file will be generated.',
type: 'string',
// string == unicode
// text == a long textarea string
// integer == int
// number == float
enum: [
'string',
'text',
'integer',
'number',
'boolean',
'datetime',
'file',
'password',
'copy',
'code',
'json',
],
},
required: {
description: 'If this value is required or not.',
type: 'boolean',
},
default: {
description:
'A default value that is saved the first time a Zap is created.',
type: 'string',
minLength: 1,
},
list: {
description:
'Acts differently when used in inputFields vs. when used in outputFields. In inputFields: Can a user provide multiples of this field? In outputFields: Does this field return an array of items of type `type`?',
type: 'boolean',
},
children: {
type: 'array',
items: { $ref: '/PlainFieldSchema' },
description:
'An array of child fields that define the structure of a sub-object for this field. Usually used for line items.',
minItems: 1,
},
dict: {
description: 'Is this field a key/value input?',
type: 'boolean',
},
},
examples: [
// 1. Minimal valid example
{ key: 'abc' },
// 2. Has a label
{ key: 'abc_label', label: 'First Name' },
// 3. Has a type and required
{ key: 'abc_required', type: 'boolean', required: true },
// 4. Has a default
{ key: 'abc_default', default: 'some default' },
// 5. Children array referencing PlainFieldSchema
{ key: 'parent', children: [{ key: 'child' }] },
// 6. A field with type=integer
{ key: 'abc_int', type: 'integer' },
// 7. A field with type=json
{ key: 'abc_json', type: 'json' },
],
antiExamples: [
{
example: {},
reason: 'Missing required key: key',
},
{
example: { key: 'abc', type: 'loltype' },
reason: 'Invalid value for key: type',
},
{
// If someone tries to add a "choices" property, it's invalid
example: { key: 'abc', choices: { mobile: 'Mobile Phone' } },
reason: 'Invalid extra property: choices (not in schema)',
},
{
// default must be at least 1 character long
example: { key: 'abc', default: '' },
reason: 'Invalid value for key: default (cannot be empty string)',
},
{
// key must be at least 1 character long
example: { key: '' },
reason: 'Invalid value for key: key (cannot be empty string)',
},
{
// children array must have at least one valid item
example: { key: 'abc', children: [] },
reason:
'Invalid value for key: children (array must have at least 1 item)',
},
{
// children must be objects that match the PlainFieldSchema
example: { key: 'abc', children: ['$func$2$f$'] },
reason:
'Invalid value for key: children (each item must be a valid PlainFieldSchema object)',
},
{
// Another example of an invalid extra property
example: { key: 'abc', helpText: 'Not allowed' },
reason: 'Invalid extra property: helpText',
},
],
additionalProperties: false,
});

View file

@ -0,0 +1,219 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const RefResourceSchema = require('./RefResourceSchema');
const FieldChoicesSchema = require('./FieldChoicesSchema');
const FieldDynamicChoicesSchema = require('./FieldDynamicChoicesSchema');
const PlainFieldSchema = require('./PlainFieldSchema');
const FieldMetaSchema = require('./FieldMetaSchema');
const KeySchema = require('./KeySchema');
const JsonSchemaSchema = require('./JsonSchemaSchema');
module.exports = makeSchema(
{
description: `Field schema specialized for input fields. ${PlainFieldSchema.schema.description}`,
id: '/PlainInputFieldSchema',
type: 'object',
required: ['key'],
properties: {
...PlainFieldSchema.schema.properties,
children: {
type: 'array',
items: { $ref: '/PlainInputFieldSchema' },
description:
'An array of child fields that define the structure of a sub-object for this field. Usually used for line items.',
minItems: 1,
},
helpText: {
description:
'A human readable description of this value (IE: "The first part of a full name."). You can use Markdown.',
type: 'string',
minLength: 1,
maxLength: 1000,
},
search: {
description:
'A reference to a search that will guide the user to add a search step to populate this field when creating a Zap.',
$ref: RefResourceSchema.id,
},
dynamic: {
description:
'A reference to a trigger that will power a dynamic dropdown.',
$ref: RefResourceSchema.id,
},
dependsOn: {
description:
"Specifies which other input fields this field depends on. These must be filled before this one becomes enabled, and when their values change, this field's value should be cleared.",
type: 'array',
items: { type: 'string' },
},
resource: {
description:
'Explicitly links this input field to a resource. Use the resource key (e.g., "spreadsheet") or dot notation for resource fields (e.g., "spreadsheet.url"). If not set for dynamic dropdowns, the resource is derived implicitly from the `dynamic` property.',
type: 'string',
minLength: 1,
pattern: '^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)?$',
},
choices: {
description:
'Describes how to populate this dropdown. Can be a static list or a dynamic object with pagination and search support.',
oneOf: [
{ $ref: FieldChoicesSchema.id },
{ $ref: FieldDynamicChoicesSchema.id },
],
},
placeholder: {
description: 'An example value that is not saved.',
type: 'string',
minLength: 1,
},
altersDynamicFields: {
description:
'Does the value of this field affect the definitions of other fields in the set?',
type: 'boolean',
},
computed: {
description:
'Is this field automatically populated (and hidden from the user)? Note: Only OAuth, Session Auth, and certain internal use cases support fields with this key.',
type: 'boolean',
},
inputFormat: {
description:
'Useful when you expect the input to be part of a longer string. Put "{{input}}" in place of the user\'s input (IE: "https://{{input}}.yourdomain.com").',
type: 'string',
// TODO: Check if it contains one and ONLY ONE '{{input}}'
pattern: '^.*{{input}}.*$',
},
meta: {
description:
'Allows for additional metadata to be stored on the field. Supports simple key-values only (no sub-objects or arrays).',
$ref: FieldMetaSchema.id,
},
group: {
description:
"References a group key from the operation's inputFieldGroups to organize this field with others.",
$ref: KeySchema.id,
},
schema: {
description:
'A JSON Schema object that describes the expected structure of the JSON value. Only valid when `type` is `json`.',
$ref: JsonSchemaSchema.id,
},
},
examples: [
{ key: 'abc' },
{ key: 'abc', choices: { mobile: 'Mobile Phone' } },
{ key: 'abc', choices: ['first', 'second', 'third'] },
{
key: 'abc',
choices: [{ label: 'Red', sample: '#f00', value: '#f00' }],
},
{
key: 'abc',
choices: { perform: '$func$0$f$' },
},
{ key: 'abc', children: [{ key: 'abc' }] },
{ key: 'abc', type: 'integer' },
{
key: 'abc',
type: 'integer',
meta: {
internalType: 'numeric',
should_call_api: true,
display_order: 1,
},
},
{
key: 'name',
group: 'contact',
},
{
key: 'email',
group: 'contact',
},
{
key: 'payload',
type: 'json',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
},
},
},
{
key: 'spreadsheet',
dependsOn: ['folder'],
},
{
key: 'worksheet',
dependsOn: ['folder', 'spreadsheet'],
},
{
key: 'spreadsheet_id',
resource: 'spreadsheet',
choices: { perform: '$func$0$f$' },
},
],
antiExamples: [
{
example: {},
reason: 'Missing required key: key',
},
{
example: { key: 'abc', type: 'loltype' },
reason: 'Invalid value for key: type',
},
{
example: { key: 'abc', choices: {} },
reason: 'Invalid value for key: choices (cannot be empty)',
},
{
example: { key: 'abc', choices: [] },
reason: 'Invalid value for key: choices (cannot be empty)',
},
{
example: { key: 'abc', choices: [3] },
reason:
'Invalid value for key: choices (if an array, must be of either string or FieldChoiceWithLabelSchema)',
},
{
example: { key: 'abc', choices: [{ label: 'Red', value: '#f00' }] },
reason:
'Invalid value for key: choices (if an array of FieldChoiceWithLabelSchema, must provide key `sample`)',
},
{
example: { key: 'abc', choices: 'mobile' },
reason:
'Invalid value for key: choices (must be either object or array)',
},
{
example: { key: 'abc', type: 'string', schema: { type: 'object' } },
reason: 'schema is only valid when type is json',
},
{
example: {
key: 'abc',
type: 'json',
schema: { type: 'foobar' },
},
reason: 'schema must contain a valid JSON Schema (invalid type)',
},
{
example: { key: 'abc', children: ['$func$2$f$'] },
reason:
'Invalid value for key: children (must be array of PlainInputFieldSchema)',
},
],
additionalProperties: false,
},
[
RefResourceSchema,
FieldChoicesSchema,
FieldDynamicChoicesSchema,
FieldMetaSchema,
PlainFieldSchema,
KeySchema,
JsonSchemaSchema,
],
);

View file

@ -0,0 +1,124 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const PlainFieldSchema = require('./PlainFieldSchema');
module.exports = makeSchema(
{
description: `Field schema specialized for output fields. ${PlainFieldSchema.schema.description}`,
id: '/PlainOutputFieldSchema',
type: 'object',
required: ['key'],
properties: {
...PlainFieldSchema.schema.properties,
children: {
type: 'array',
items: { $ref: '/PlainOutputFieldSchema' },
description:
'An array of child fields that define the structure of a sub-object for this field. Usually used for line items.',
minItems: 1,
},
type: {
description:
'The type of this value. Field type of `file` will accept either a file object or a string. If a URL is provided in the string, Zapier will automatically make a GET for that file. Otherwise, a .txt file will be generated.',
type: 'string',
// string == unicode
// number == float
enum: [
'string',
'number',
'boolean',
'datetime',
'file',
'password',
'integer',
],
},
primary: {
description:
'Use this field as part of the primary key for deduplication. You can set multiple fields as "primary", provided they are unique together. If no fields are set, Zapier will default to using the `id` field. `primary` only makes sense for `outputFields`. It only works in static `outputFields`; will not work in custom/dynamic `outputFields`. For more information, see [How deduplication works in Zapier](https://platform.zapier.com/build/deduplication).',
type: 'boolean',
},
steadyState: {
description:
'Prevents triggering on new output until all values for fields with this property remain unchanged for 2 polls. It can be used to, e.g., not trigger on a new contact until the contact has completed typing their name. NOTE that this only applies to the `outputFields` of polling triggers.',
type: 'boolean',
},
sample: {
description:
'An example value for this field. Can be any type (string, number, boolean, object, array, null) to match the expected field output. Values provided here will be combined with values in the operation level `sample` field, with this field taking precedence. This is most useful when using a function to generate dynamic `outputFields`.',
oneOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean' },
{ type: 'object' },
{ type: 'array' },
{ type: 'null' },
],
},
},
examples: [
{ key: 'abc' },
{ key: 'abc', children: [{ key: 'abc' }] },
{ key: 'abc', type: 'number', label: 'neat' },
{ key: 'abc', type: 'number' },
{ key: 'name', type: 'string', sample: 'John Doe' },
{ key: 'price', type: 'number', sample: 29.99 },
{ key: 'is_active', type: 'boolean', sample: true },
{ key: 'tags', list: true, sample: ['work', 'urgent'] },
{
key: 'metadata',
sample: { created_at: '2025-01-15', author: 'system' },
},
{
key: 'address',
sample: { street: '123 Main St', city: 'London' },
children: [
{ key: 'street', sample: '123 Main St' },
{ key: 'city', sample: 'London' },
],
},
],
antiExamples: [
{
example: {},
reason: 'Missing required key: key',
},
{
example: { key: 'abc', type: 'loltype' },
reason: 'Invalid value for key: type',
},
{
example: { key: 'abc', choices: {} },
reason: 'Invalid value for key: choices (cannot be empty)',
},
{
example: { key: 'abc', choices: [] },
reason: 'Invalid value for key: choices (cannot be empty)',
},
{
example: { key: 'abc', choices: [3] },
reason:
'Invalid value for key: choices (if an array, must be of either string or FieldChoiceWithLabelSchema)',
},
{
example: { key: 'abc', choices: [{ label: 'Red', value: '#f00' }] },
reason:
'Invalid value for key: choices (if an array of FieldChoiceWithLabelSchema, must provide key `sample`)',
},
{
example: { key: 'abc', choices: 'mobile' },
reason:
'Invalid value for key: choices (must be either object or array)',
},
{
example: { key: 'abc', children: ['$func$2$f$'] },
reason:
'Invalid value for key: children (must be array of PlainOutputFieldSchema)',
},
],
additionalProperties: false,
},
[PlainFieldSchema],
);

View file

@ -0,0 +1,41 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FlatObjectSchema = require('./FlatObjectSchema');
module.exports = makeSchema(
{
id: '/RedirectRequestSchema',
description:
'A representation of a HTTP redirect - you can use the `{{syntax}}` to inject authentication, field or global variables.',
type: 'object',
properties: {
method: {
description: 'The HTTP method for the request.',
type: 'string',
default: 'GET',
enum: ['GET'],
},
url: {
description:
'A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.',
type: 'string',
},
params: {
description:
'A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.',
$ref: FlatObjectSchema.id,
},
},
additionalProperties: false,
examples: [{ method: 'GET', url: 'https://google.com' }],
antiExamples: [
{
example: { method: 'POST', url: 'https://google.com' },
reason: 'Invalid value for key: method',
},
],
},
[FlatObjectSchema],
);

View file

@ -0,0 +1,35 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/RefResourceSchema',
description:
'Reference a resource by key and the data it returns. In the format of: `{resource_key}.{foreign_key}(.{human_label_key})`.',
type: 'string',
// the human_label_key should match the broad `string` type that PlainFieldSchema.key can be, with commas!
pattern:
'^[a-zA-Z0-9_]+\\.[a-zA-Z0-9_\\s\\[\\]]+(\\.[a-zA-Z0-9_\\s\\[\\]]+(,[a-zA-Z0-9_\\s\\[\\]]+)*)?$',
examples: [
'contact.id',
'contact.id.name',
'contact.id.firstName,lastName',
'contact.id.first_name,last_name,email',
'contact.Contact Id.Full Name',
'contact.data[]id.data[]First Name,data[]Last Name',
],
antiExamples: [
{
example: 'Contact List',
reason: 'Does not match resource_key pattern',
},
{
example: 'Contact.list,find.id',
reason: 'Does not match foreign_key pattern',
},
{
example: 'Contact.list.id.full_name',
reason: 'Does not match human_label_key pattern',
},
],
});

View file

@ -0,0 +1,114 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const FlatObjectSchema = require('./FlatObjectSchema');
const FunctionSchema = require('./FunctionSchema');
module.exports = makeSchema(
{
id: '/RequestSchema',
description:
'A representation of a HTTP request - you can use the `{{syntax}}` to inject authentication, field or global variables.',
type: 'object',
properties: {
method: {
description: 'The HTTP method for the request.',
type: 'string',
default: 'GET',
enum: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'HEAD'],
},
url: {
description:
'A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.',
type: 'string',
},
body: {
description: 'Can be nothing, a raw string or JSON (object or array).',
oneOf: [
{ type: 'null' }, // nothing
{ type: 'string' }, // raw body
{ type: 'object' }, // json body object
{ type: 'array' }, // json body array
],
},
params: {
description:
'A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.',
$ref: FlatObjectSchema.id,
},
headers: {
description: 'The HTTP headers for the request.',
$ref: FlatObjectSchema.id,
},
auth: {
description:
"An object holding the auth parameters for OAuth1 request signing, like `{oauth_token: 'abcd', oauth_token_secret: '1234'}`. Or an array reserved (i.e. not implemented yet) to hold the username and password for Basic Auth. Like `['AzureDiamond', 'hunter2']`.",
oneOf: [
{
type: 'array',
items: {
type: 'string',
minProperties: 2,
maxProperties: 2,
},
},
{ $ref: FlatObjectSchema.id },
],
},
removeMissingValuesFrom: {
description:
'Should missing values be sent? (empty strings, `null`, and `undefined` only — `[]`, `{}`, and `false` will still be sent). Allowed fields are `params` and `body`. The default is `false`, ex: ```removeMissingValuesFrom: { params: false, body: false }```',
type: 'object',
properties: {
params: {
description:
'Refers to data sent via a requests query params (`req.params`)',
type: 'boolean',
default: false,
},
body: {
description:
'Refers to tokens sent via a requsts body (`req.body`)',
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
serializeValueForCurlies: {
description:
'A function to customize how to serialize a value for curlies `{{var}}` in the request object. By default, when this is unspecified, the request client only replaces curlies where variables are strings, and would throw an error for non-strings. The function should accepts a single argument as the value to be serialized and return the string representation of the argument.',
$ref: FunctionSchema.id,
},
skipThrowForStatus: {
description:
"If `true`, don't throw an exception for response 400 <= status < 600 automatically before resolving with the response. Defaults to `false`.",
type: 'boolean',
default: false,
},
skipEncodingChars: {
description:
'Contains the characters that you want left unencoded in the query params (`req.params`). If unspecified, `z.request()` will percent-encode non-ascii characters and these reserved characters: ``:$/?#[]@$&+,;=^@`\\``.',
type: 'string',
},
},
additionalProperties: false,
examples: [
{
method: 'GET',
url: 'https://zapier.com',
},
],
antiExamples: [
{
example: {
method: 'SUPERCHARGE',
url: 'https://zapier.com',
},
reason: 'Invalid value for key: method',
},
],
},
[FlatObjectSchema, FunctionSchema],
);

View file

@ -0,0 +1,67 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
module.exports = makeSchema(
{
id: '/ResourceMethodCreateSchema',
description:
'How will we find create a specific object given inputs? Will be turned into a create automatically.',
type: 'object',
required: ['display', 'operation'],
properties: {
display: {
description: 'Define how this create method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this create method will work.',
$ref: BasicActionOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
display: {
label: 'Create Tag',
description: 'Create a new Tag in your account.',
},
operation: {
perform: '$func$2$f$',
sample: {
id: 1,
},
},
},
{
display: {
label: 'Create Tag',
description: 'Create a new Tag in your account.',
hidden: true,
},
operation: {
perform: '$func$2$f$',
},
},
],
antiExamples: [
{
example: {
display: {
label: 'Create Tag',
description: 'Create a new Tag in your account.',
},
operation: {
perform: '$func$2$f$',
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicActionOperationSchema],
);

View file

@ -0,0 +1,74 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicOperationSchema = require('./BasicOperationSchema');
module.exports = makeSchema(
{
id: '/ResourceMethodGetSchema',
description:
'How will we get a single object given a unique identifier/id?',
type: 'object',
required: ['display', 'operation'],
properties: {
display: {
description: 'Define how this get method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this get method will work.',
$ref: BasicOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: '$func$0$f$',
},
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
{
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
hidden: true,
},
operation: {
perform: {
url: '$func$0$f$',
},
},
},
],
antiExamples: [
{
example: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: '$func$0$f$',
},
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicOperationSchema],
);

View file

@ -0,0 +1,72 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicHookOperationSchema = require('./BasicHookOperationSchema');
module.exports = makeSchema(
{
id: '/ResourceMethodHookSchema',
description:
'How will we get notified of new objects? Will be turned into a trigger automatically.',
type: 'object',
required: ['display', 'operation'],
properties: {
display: {
description:
'Define how this hook/trigger method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this hook/trigger method will work.',
$ref: BasicHookOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
type: 'hook',
perform: '$func$0$f$',
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
{
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
hidden: true,
},
operation: {
type: 'hook',
perform: '$func$0$f$',
},
},
],
antiExamples: [
{
example: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
type: 'hook',
perform: '$func$0$f$',
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicHookOperationSchema],
);

View file

@ -0,0 +1,76 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicPollingOperationSchema = require('./BasicPollingOperationSchema');
module.exports = makeSchema(
{
id: '/ResourceMethodListSchema',
description:
'How will we get a list of new objects? Will be turned into a trigger automatically.',
type: 'object',
required: ['display', 'operation'],
properties: {
display: {
description:
'Define how this list/trigger method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this list/trigger method will work.',
$ref: BasicPollingOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
display: {
label: 'New User',
description: 'Trigger when a new User is created in your account.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/users',
},
sample: {
id: 49,
name: 'Veronica Kuhn',
email: 'veronica.kuhn@company.com',
},
},
},
{
display: {
label: 'New User',
description: 'Trigger when a new User is created in your account.',
hidden: true,
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/users',
},
},
},
],
antiExamples: [
{
example: {
display: {
label: 'New User',
description: 'Trigger when a new User is created in your account.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/users',
},
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicPollingOperationSchema],
);

View file

@ -0,0 +1,67 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
module.exports = makeSchema(
{
id: '/ResourceMethodSearchSchema',
description:
'How will we find a specific object given filters or search terms? Will be turned into a search automatically.',
type: 'object',
required: ['display', 'operation'],
properties: {
display: {
description: 'Define how this search method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this search method will work.',
$ref: BasicActionOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
{
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
hidden: true,
},
operation: {
perform: '$func$2$f$',
},
},
],
antiExamples: [
{
example: {
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
},
operation: {
perform: '$func$2$f$',
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicActionOperationSchema],
);

View file

@ -0,0 +1,168 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const ResourceMethodGetSchema = require('./ResourceMethodGetSchema');
const ResourceMethodHookSchema = require('./ResourceMethodHookSchema');
const ResourceMethodListSchema = require('./ResourceMethodListSchema');
const ResourceMethodSearchSchema = require('./ResourceMethodSearchSchema');
const ResourceMethodCreateSchema = require('./ResourceMethodCreateSchema');
const KeySchema = require('./KeySchema');
const OutputFieldsSchema = require('./OutputFieldsSchema');
module.exports = makeSchema(
{
id: '/ResourceSchema',
description:
'Represents a resource, which will in turn power triggers, searches, or creates.',
type: 'object',
required: ['key', 'noun'],
properties: {
key: {
description: 'A key to uniquely identify this resource.',
$ref: KeySchema.id,
},
noun: {
description:
'A noun for this resource that completes the sentence "create a new XXX".',
type: 'string',
minLength: 2,
maxLength: 255,
},
// TODO: do we need to break these all apart too? :-/
get: {
description: ResourceMethodGetSchema.schema.description,
$ref: ResourceMethodGetSchema.id,
},
hook: {
description: ResourceMethodHookSchema.schema.description,
$ref: ResourceMethodHookSchema.id,
},
list: {
description: ResourceMethodListSchema.schema.description,
$ref: ResourceMethodListSchema.id,
},
search: {
description: ResourceMethodSearchSchema.schema.description,
$ref: ResourceMethodSearchSchema.id,
},
create: {
description: ResourceMethodCreateSchema.schema.description,
$ref: ResourceMethodCreateSchema.id,
},
outputFields: {
description: 'What fields of data will this return?',
$ref: OutputFieldsSchema.id,
},
sample: {
description: 'What does a sample of data look like?',
type: 'object',
// TODO: require id, ID, Id property?
minProperties: 1,
},
},
additionalProperties: false,
examples: [
{
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
},
{
key: 'tag',
noun: 'Tag',
sample: {
id: 385,
name: 'proactive enable ROI',
},
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
// resource sample is used
},
},
},
{
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
hidden: true,
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
},
},
list: {
display: {
label: 'New Tag',
description: 'Trigger when a new Tag is created in your account.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags',
},
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
},
],
antiExamples: [
{
example: {
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
},
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[
ResourceMethodGetSchema,
ResourceMethodHookSchema,
ResourceMethodListSchema,
ResourceMethodSearchSchema,
ResourceMethodCreateSchema,
OutputFieldsSchema,
KeySchema,
],
);

View file

@ -0,0 +1,57 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicActionOperationSchema = require('./BasicActionOperationSchema');
module.exports = makeSchema(
{
id: '/ResourcesMethodGetSchema',
description: 'How will we get a batch of objects?',
type: 'objects',
required: ['display', 'operation'],
examples: [
{
display: {
label: 'Get Users',
description: 'Retrieve an index of users.',
},
operation: {
display: 'Fetch users',
perform: '$func$2$f$',
sample: {
id: 1,
firstName: 'Walter',
lastName: 'Sobchak',
occupation: 'Bowler',
},
},
},
],
antiExamples: [
{
display: {
label: 'Get Users',
description: 'Retrieve an index of users.',
},
operation: {
description: 'Define how this search method will work.',
$ref: BasicActionOperationSchema.id,
},
},
],
properties: {
display: {
description: 'Define how this get method will be exposed in the UI.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Define how this get method will work.',
$ref: BasicActionOperationSchema.id,
},
},
additionalProperties: false,
},
[BasicDisplaySchema, BasicActionOperationSchema],
);

View file

@ -0,0 +1,95 @@
'use strict';
const { SKIP_KEY } = require('../constants');
const makeSchema = require('../utils/makeSchema');
const ResourceSchema = require('./ResourceSchema');
module.exports = makeSchema(
{
id: '/ResourcesSchema',
description:
'All the resources that underlie common CRUD methods powering automatically handled triggers, creates, and searches for your app. Zapier will break these apart for you.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the ResourceSchema.',
$ref: ResourceSchema.id,
},
},
additionalProperties: false,
examples: [
{
tag: {
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
},
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that keys don't match
example: {
getTag: {
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
sample: {
id: 385,
name: 'proactive enable ROI',
},
},
},
},
},
reason: 'Key does not match key for associated /ResourceSchema',
},
{
[SKIP_KEY]: true, // Cannot validate that sample is only required if display isn't true / top-level resource doesn't have sample
example: {
tag: {
key: 'tag',
noun: 'Tag',
get: {
display: {
label: 'Get Tag by ID',
description: 'Grab a specific Tag by ID.',
},
operation: {
perform: {
url: 'https://fake-crm.getsandbox.com/tags/{{inputData.id}}',
},
},
},
},
},
reason:
'Missing key from operation: sample. Note  this is valid if the resource has defined a sample.',
},
],
},
[ResourceSchema],
);

View file

@ -0,0 +1,25 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/ResultsSchema',
description: 'An array of objects suitable for returning in perform calls.',
type: 'array',
items: {
type: 'object',
// TODO: require id, ID, Id property?
minProperties: 1,
},
examples: [[{ name: 'Alex Trebek' }]],
antiExamples: [
{
example: 1,
reason: 'Invalid type (must be array)',
},
{
example: [1],
reason: 'Invalid type (must be array of objects)',
},
],
});

View file

@ -0,0 +1,14 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const SearchOrCreatesSchema = require('./SearchOrCreatesSchema');
module.exports = makeSchema(
{
...SearchOrCreatesSchema.schema,
id: '/SearchAndCreatesSchema',
description: 'Alias for /SearchOrCreatesSchema',
},
[SearchOrCreatesSchema],
);

View file

@ -0,0 +1,138 @@
'use strict';
const { SKIP_KEY } = require('../constants');
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const FlatObjectSchema = require('./FlatObjectSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/SearchOrCreateSchema',
description:
'Pair an existing search and a create to enable "Find or Create" functionality in your app',
type: 'object',
required: ['key', 'display', 'search', 'create'],
properties: {
key: {
description:
'A key to uniquely identify this search-or-create. Must match the search key.',
$ref: KeySchema.id,
},
display: {
description: 'Configures the UI for this search-or-create.',
$ref: BasicDisplaySchema.id,
},
search: {
description: 'The key of the search that powers this search-or-create',
$ref: KeySchema.id,
},
create: {
description: 'The key of the create that powers this search-or-create',
$ref: KeySchema.id,
},
update: {
description:
'EXPERIMENTAL: The key of the update action (in `creates`) that will be used if a search succeeds.',
$ref: KeySchema.id,
},
updateInputFromSearchOutput: {
description:
"EXPERIMENTAL: A mapping where the key represents the input field for the update action, and the value represents the field from the search action's output that should be mapped to the update action's input field.",
$ref: FlatObjectSchema.id,
},
searchUniqueInputToOutputConstraint: {
description:
"EXPERIMENTAL: A mapping where the key represents an input field for the search action, and the value represents how that field's value will be used to filter down the search output for an exact match.",
type: 'object',
},
},
additionalProperties: false,
examples: [
{
key: 'searchOrCreateWidgets',
display: {
label: 'Search or Create Widgets',
description:
'Searches for a widget matching the provided query, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
},
{
key: 'upsertWidgets',
display: {
label: 'Upsert Widgets',
description:
'Searches for a widget matching the provided query and updates it if found, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
update: 'updateExistingWidget',
updateInputFromSearchOutput: {
widget_id: 'id',
},
searchUniqueInputToOutputConstraint: {
widget_name: 'name',
},
},
],
antiExamples: [
{
example: {
key: '01_Search_or_Create_Widgets',
display: {
label: 'Search or Create Widgets',
description:
'Searches for a widget matching the provided query, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
},
reason: 'Invalid value for key: key (must start with a letter)',
},
{
example: {
key: 'searchOrCreateWidgets',
display: {
label: 'Search or Create Widgets',
description:
'Searches for a widget matching the provided query, or creates one if it does not exist.',
hidden: false,
},
search: { require: 'path/to/some/file.js' },
create: { require: 'path/to/some/file.js' },
},
reason:
'Invalid values for keys: search and create (must be a string that matches the key of a registered search or create)',
},
{
[SKIP_KEY]: true, // Cannot validate field dependency between updateInputFromSearchOutput / searchUniqueInputToOutputConstraint and update
example: {
key: 'upsertWidgets',
display: {
label: 'Upsert Widgets',
description:
'Searches for a widget matching the provided query and updates it if found, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
updateInputFromSearchOutput: {
widget_id: 'id',
},
searchUniqueInputToOutputConstraint: {
widget_name: 'name',
},
},
reason:
'If either the updateInputFromSearchOutput or searchUniqueInputToOutputConstraint keys are present, then the update key must be present as well.',
},
],
},
[BasicDisplaySchema, KeySchema, FlatObjectSchema],
);

View file

@ -0,0 +1,73 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const SearchOrCreateSchema = require('./SearchOrCreateSchema');
module.exports = makeSchema(
{
id: '/SearchOrCreatesSchema',
description:
'Enumerates the search-or-creates your app has available for users.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the SearchOrCreateSchema.',
$ref: SearchOrCreateSchema.id,
},
},
additionalProperties: false,
examples: [
{
searchOrCreateWidgets: {
key: 'searchOrCreateWidgets',
display: {
label: 'Search or Create Widgets',
description:
'Searches for a widget matching the provided query, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
},
},
{
searchAndCreateWidgets: {
key: 'searchAndCreateWidgets',
display: {
label: 'Search and Create Widgets',
description:
'Searches for a widget matching the provided query, creates one if it does not exist or updates existing one if found.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
update: 'updateWidget',
},
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that keys match
example: {
searchOrCreateWidgets: {
key: 'socWidgets',
display: {
label: 'Search or Create Widgets',
description:
'Searches for a widget matching the provided query, or creates one if it does not exist.',
hidden: false,
},
search: 'searchWidgets',
create: 'createWidget',
},
},
reason:
'Key must match the key of the associated /SearchOrCreateSchema',
},
],
},
[SearchOrCreateSchema],
);

View file

@ -0,0 +1,84 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicSearchOperationSchema = require('./BasicSearchOperationSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/SearchSchema',
description: 'How will Zapier search for existing objects?',
type: 'object',
required: ['key', 'noun', 'display', 'operation'],
properties: {
key: {
description: 'A key to uniquely identify this search.',
$ref: KeySchema.id,
},
noun: {
description:
'A noun for this search that completes the sentence "finds a specific XXX".',
type: 'string',
minLength: 2,
maxLength: 255,
},
display: {
description: 'Configures the UI for this search.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Powers the functionality for this search.',
$ref: BasicSearchOperationSchema.id,
},
},
additionalProperties: false,
examples: [
{
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
},
operation: {
perform: '$func$2$f$',
sample: { id: 1 },
},
},
{
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
hidden: true,
},
operation: { perform: '$func$2$f$' },
},
],
antiExamples: [
{
example: 'abc',
reason: 'Must be an object',
},
{
example: {
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
},
operation: {
perform: '$func$2$f$',
},
},
reason:
'Missing required key in operation: sample. Note - this is valid if the associated resource has defined a sample.',
},
],
},
[BasicDisplaySchema, BasicSearchOperationSchema, KeySchema],
);

View file

@ -0,0 +1,55 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const SearchSchema = require('./SearchSchema');
module.exports = makeSchema(
{
id: '/SearchesSchema',
description: 'Enumerates the searches your app has available for users.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the SearchSchema.',
$ref: SearchSchema.id,
},
},
additionalProperties: false,
examples: [
{
recipe: {
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
hidden: true,
},
operation: { perform: '$func$2$f$' },
},
},
],
antiExamples: [
{
[SKIP_KEY]: true, // Cannot validate that keys match
example: {
searchRecipe: {
key: 'recipe',
noun: 'Recipe',
display: {
label: 'Find a Recipe',
description: 'Search for recipe by cuisine style.',
hidden: true,
},
operation: { perform: '$func$2$f$' },
},
},
reason: 'Key must match the key of the associated /SearchSchema',
},
],
},
[SearchSchema],
);

View file

@ -0,0 +1,131 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const ThrottleOverrideObjectSchema = require('./ThrottleOverrideObjectSchema');
module.exports = makeSchema(
{
id: '/ThrottleObjectSchema',
description:
'Zapier uses this configuration to apply throttling when the limit for the window is exceeded. **NOTE:** The final key used for the throttling is formed as a combination of all the configurations; key, window, limit, and scope. To share a limit across multiple actions in an integration, each should have the same configuration set without "action" in the scope.',
type: 'object',
required: ['window', 'limit'],
properties: {
window: {
description:
'The timeframe, in seconds, within which the system tracks the number of invocations for an action. The number of invocations begins at zero at the start of each window.',
type: 'integer',
},
limit: {
description:
'The maximum number of invocations for an action, allowed within the timeframe window.',
type: 'integer',
},
key: {
description:
'The key to throttle with in combination with the scope. User data provided for the input fields can be used in the key with the use of the curly braces referencing. For example, to access the user data provided for the input field "test_field", use `{{bundle.inputData.test_field}}`. Note that a required input field should be referenced to get user data always.',
type: 'string',
minLength: 1,
},
scope: {
description: `The granularity to throttle by. You can set the scope to one or more of the following: 'user' - Throttles based on user ids. 'auth' - Throttles based on auth ids. 'account' - Throttles based on account ids for all users under a single account. 'action' - Throttles the action it is set on separately from other actions. By default, throttling is scoped to the action and account.`,
type: 'array',
items: {
enum: ['user', 'auth', 'account', 'action'],
type: 'string',
},
},
retry: {
description:
'The effect of throttling on the tasks of the action. `true` means throttled tasks are automatically retried after some delay, while `false` means tasks are held without retry. It defaults to `true`. NOTE that it has no effect on polling triggers and should not be set.',
type: 'boolean',
},
filter: {
description: `EXPERIMENTAL: Account-based attribute to override the throttle by. You can set to one of the following: "free", "trial", "paid". Therefore, the throttle scope would be automatically set to "account" and ONLY the accounts based on the specified filter will have their requests throttled based on the throttle overrides while the rest are throttled based on the original configuration.`,
type: 'string',
enum: ['free', 'trial', 'paid'],
},
overrides: {
description:
'EXPERIMENTAL: Overrides the original throttle configuration based on a Zapier account attribute.',
type: 'array',
minItems: 1,
items: {
$ref: ThrottleOverrideObjectSchema.id,
},
},
},
examples: [
{
window: 60,
limit: 100,
},
{
window: 600,
limit: 100,
scope: ['account', 'user'],
},
{
window: 3600,
limit: 10,
scope: ['auth'],
},
{
window: 3600,
limit: 10,
key: 'random_key',
scope: [], // this ensures neither the default nor any of the scope options is used
},
{
window: 3600,
limit: 10,
key: 'random_key-{{bundle.inputData.test_field}}',
scope: ['action', 'auth'],
retry: false,
},
{
window: 3600,
limit: 10,
scope: ['auth'],
retry: false,
overrides: [
{
window: 3600,
limit: 2,
filter: 'free',
retry: false,
},
],
},
],
antiExamples: [
{
example: {
window: 60,
limit: 100,
scope: ['zap'],
},
reason: 'Invalid scope provided: `zap`.',
},
{
example: { limit: 10 },
reason: 'Missing required key: `window`.',
},
{
example: { window: 600 },
reason: 'Missing required key: `limit`.',
},
{
example: { window: 600, limit: 100, overrides: [] },
reason: 'The overrides needs at least one item.',
},
{
example: {},
reason: 'Missing required keys: `window` and `limit`.',
},
],
additionalProperties: false,
},
[ThrottleOverrideObjectSchema],
);

View file

@ -0,0 +1,71 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/ThrottleOverrideObjectSchema',
description:
'EXPERIMENTAL: Overrides the original throttle configuration based on a Zapier account attribute.',
type: 'object',
required: ['window', 'limit', 'filter'],
properties: {
window: {
description:
'The timeframe, in seconds, within which the system tracks the number of invocations for an action. The number of invocations begins at zero at the start of each window.',
type: 'integer',
},
limit: {
description:
'The maximum number of invocations for an action, allowed within the timeframe window.',
type: 'integer',
},
filter: {
description: `Account-based attribute to override the throttle by. You can set to one of the following: "free", "trial", "paid". Therefore, the throttle scope would be automatically set to "account" and ONLY the accounts based on the specified filter will have their requests throttled based on the throttle overrides while the rest are throttled based on the original configuration.`,
type: 'string',
enum: ['free', 'trial', 'paid'],
},
retry: {
description:
'The effect of throttling on the tasks of the action. `true` means throttled tasks are automatically retried after some delay, while `false` means tasks are held without retry. It defaults to `true`. NOTE that it has no effect on polling triggers and should not be set.',
type: 'boolean',
},
},
examples: [
{
window: 60,
limit: 100,
filter: 'free',
},
{
window: 60,
limit: 100,
filter: 'paid',
retry: false,
},
{
window: 60,
limit: 100,
filter: 'trial',
retry: true,
},
],
antiExamples: [
{
example: { limit: 10 },
reason: 'Missing required key: `window` and `filter`.',
},
{
example: { window: 600 },
reason: 'Missing required key: `limit` and `filter`.',
},
{
example: { filter: 'trial' },
reason: 'Missing required key: `window` and `limit`.',
},
{
example: {},
reason: 'Missing required keys: `window`, `limit`, and `filter`.',
},
],
additionalProperties: false,
});

View file

@ -0,0 +1,96 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const BasicDisplaySchema = require('./BasicDisplaySchema');
const BasicHookOperationSchema = require('./BasicHookOperationSchema');
const BasicHookToPollOperationSchema = require('./BasicHookToPollOperationSchema');
const BasicPollingOperationSchema = require('./BasicPollingOperationSchema');
const KeySchema = require('./KeySchema');
module.exports = makeSchema(
{
id: '/TriggerSchema',
description: 'How will Zapier get notified of new objects?',
type: 'object',
required: ['key', 'noun', 'display', 'operation'],
properties: {
key: {
description: 'A key to uniquely identify this trigger.',
$ref: KeySchema.id,
},
noun: {
description:
'A noun for this trigger that completes the sentence "triggers on a new XXX".',
type: 'string',
minLength: 2,
maxLength: 255,
},
display: {
description: 'Configures the UI for this trigger.',
$ref: BasicDisplaySchema.id,
},
operation: {
description: 'Powers the functionality for this trigger.',
anyOf: [
{ $ref: BasicPollingOperationSchema.id },
{ $ref: BasicHookOperationSchema.id },
{ $ref: BasicHookToPollOperationSchema.id },
],
},
},
additionalProperties: false,
examples: [
{
key: 'new_recipe',
noun: 'Recipe',
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.',
},
operation: {
type: 'polling',
perform: '$func$0$f$',
sample: { id: 1 },
},
},
{
key: 'new_recipe',
noun: 'Recipe',
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.',
hidden: true,
},
operation: {
type: 'polling',
perform: '$func$0$f$',
},
},
],
antiExamples: [
{
example: {
key: 'new_recipe',
noun: 'Recipe',
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.',
},
operation: {
perform: '$func$0$f$',
},
},
reason:
'Missing required key from operation: sample. Note - this is valid if the Recipe resource has defined a sample.',
},
],
},
[
KeySchema,
BasicDisplaySchema,
BasicPollingOperationSchema,
BasicHookOperationSchema,
BasicHookToPollOperationSchema,
],
);

View file

@ -0,0 +1,61 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
const { SKIP_KEY } = require('../constants');
const TriggerSchema = require('./TriggerSchema');
module.exports = makeSchema(
{
id: '/TriggersSchema',
description: 'Enumerates the triggers your app has available for users.',
type: 'object',
patternProperties: {
'^[a-zA-Z]+[a-zA-Z0-9_]*$': {
description:
'Any unique key can be used and its values will be validated against the TriggerSchema.',
$ref: TriggerSchema.id,
},
},
additionalProperties: false,
examples: [
{
newRecipe: {
key: 'newRecipe',
noun: 'Recipe',
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.',
},
operation: {
type: 'polling',
perform: '$func$0$f$',
sample: { id: 1 },
},
},
},
],
antiExamples: [
{
example: {
[SKIP_KEY]: true, // Cannot validate that keys don't match
newRecipe: {
key: 'new_recipe',
noun: 'Recipe',
display: {
label: 'New Recipe',
description: 'Triggers when a new recipe is added.',
},
operation: {
type: 'polling',
perform: '$func$0$f$',
sample: { id: 1 },
},
},
},
reason: 'Key must match the key on the associated /TriggerSchema',
},
],
},
[TriggerSchema],
);

View file

@ -0,0 +1,29 @@
'use strict';
const makeSchema = require('../utils/makeSchema');
module.exports = makeSchema({
id: '/VersionSchema',
description:
'Represents a simplified semver string, from `0.0.0` to `999.999.999` with optional simplified label. They need to be case-insensitive unique.',
type: 'string',
pattern:
// this is mirrored in ZapierBaseCommand.js and developer_cli/constants.py
'^(?:0|[1-9]\\d{0,2})\\.(?:0|[1-9]\\d{0,2})\\.(?:0|[1-9]\\d{0,2})(?:-(?=.{1,12}$)[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?$',
minLength: 5,
maxLength: 24,
examples: ['1.0.0', '2.11.3', '999.999.999', '1.2.0-beta', '0.0.0-ISSUE-123'],
antiExamples: [
{ example: '1.0.0.0', reason: 'Must have 2 periods' },
{ example: '1000.0.0', reason: 'Each number can be a maximum of 3 digits' },
{ example: 'v1.0.0', reason: 'No letter prefix allowed' },
{ example: '1.0.0-rc.1', reason: 'No periods allowed in label' },
{ example: '1.0.0--', reason: 'No repeated dashes allowed' },
{
example: '1.0.0-foo--bar',
reason: 'No repeated dashes allowed in label',
},
{ example: '1.0.0-', reason: 'No empty label allowed' },
{ example: '1.0.0-foo-', reason: 'No trailing dash allowed in label' },
],
});

View file

@ -0,0 +1,231 @@
'use strict';
const util = require('util');
const _ = require('lodash');
const toc = require('markdown-toc');
const packageJson = require('../../package.json');
const links = require('./links');
const NO_DESCRIPTION = '_No description given._';
const COMBOS = ['anyOf', 'allOf', 'oneOf'];
const { SKIP_KEY } = require('../constants');
const hiddenRefs = [];
const walkSchemas = (InitSchema, callback) => {
const recurse = (Schema, parents) => {
parents = parents || [];
callback(Schema, parents);
Schema.dependencies.forEach((childSchema) => {
const newParents = parents.concat([InitSchema]);
recurse(childSchema, newParents);
});
};
recurse(InitSchema);
};
const collectSchemas = (InitSchema) => {
const schemas = {};
walkSchemas(InitSchema, (Schema) => {
if (!_.get(Schema, 'schema.docAnnotation.hide')) {
schemas[Schema.id] = Schema;
} else {
hiddenRefs.push(Schema.id);
}
});
return schemas;
};
const BREAK_LENGTH = 96;
const prepQuote = (val) => val.replace('`', '');
const quote = (val, triple, indent = '') =>
// either ``` with optional indentation or `
triple && val.length > BREAK_LENGTH
? '```\n' +
val
.match(/[^\r\n]+/g)
.map((line) => indent + line)
.join('\n') +
'\n' +
indent +
'```'
: `\`${prepQuote(val)}\``;
const quoteOrNa = (val, triple = false, indent = '') =>
val ? quote(val, triple, indent) : '_n/a_';
const formatExample = (example) => {
const ex = _.isPlainObject(example) ? _.omit(example, SKIP_KEY) : example;
return `* ${quoteOrNa(
util.inspect(ex, { depth: null, breakLength: BREAK_LENGTH }),
true,
' ',
)}`.replace(/\s+\n/gm, '\n');
};
// Generate a display of the type (or link to a $ref).
const typeOrLink = (schema) => {
if (schema.type === 'array' && schema.items) {
return `${quoteOrNa(schema.type)}[${typeOrLink(schema.items)}]`;
}
if (schema.$ref) {
if (!hiddenRefs.includes(schema.$ref)) {
return `[${schema.$ref}](${links.anchor(schema.$ref)})`;
}
return;
}
for (let i = 0; i < COMBOS.length; i++) {
const key = COMBOS[i];
if (schema[key] && schema[key].length) {
return `${key}(${schema[key]
.map(typeOrLink)
.filter(Boolean)
.join(', ')})`;
}
}
if (schema.enum && schema.enum.length) {
return `${quoteOrNa(schema.type)} in (${schema.enum
.map(util.inspect)
.map(quoteOrNa)
.join(', ')})`;
}
return quoteOrNa(schema.type);
};
// Properly quote and display examples.
const makeExampleSection = (Schema) => {
const examples = Schema.schema.examples || [];
if (!examples.length) {
return '';
}
return `\
#### Examples
${examples.map(formatExample).join('\n')}
`;
};
// Properly quote and display anti-examples.
const makeAntiExampleSection = (Schema) => {
const antiExamples = Schema.schema.antiExamples || [];
if (!antiExamples.length) {
return '';
}
return `\
#### Anti-Examples
${antiExamples
.map(({ example, reason }) => {
const formattedAntiExample = formatExample(example);
// If block quote, newline and indent the reason.
// Otherwise, show the reason inline w/ the anti-example and separated by a dash.
return formattedAntiExample.endsWith('```')
? `${formattedAntiExample}\n _${reason}_`
: `${formattedAntiExample} - _${reason}_`;
})
.join('\n')}
`;
};
const processProperty = (key, property, propIsRequired) => {
let isRequired = propIsRequired ? '**yes**' : 'no';
if (_.get(property, 'docAnnotation.hide')) {
return '';
} else if (_.get(property, 'docAnnotation.required')) {
// can also support keys besides "required"
const annotation = property.docAnnotation.required;
if (annotation.type === 'replace') {
isRequired = annotation.value;
} else if (annotation.type === 'append') {
isRequired += annotation.value;
} else {
throw new Error(`unrecognized docAnnotation type: ${annotation.type}`);
}
}
return `${quoteOrNa(key)} | ${isRequired} | ${typeOrLink(property)} | ${
property.description || NO_DESCRIPTION
}`;
};
// Enumerate the properties as a table.
const makePropertiesSection = (Schema) => {
const properties =
Schema.schema.properties || Schema.schema.patternProperties || {};
if (!Object.keys(properties).length) {
return '';
}
const required = Schema.schema.required || [];
return `\
#### Properties
Key | Required | Type | Description
--- | -------- | ---- | -----------
${Object.keys(properties)
.map((key) => {
const property = properties[key];
return processProperty(key, property, required.includes(key));
})
.join('\n')}
`;
};
// Given a "root" schema, create some markdown.
const makeMarkdownSection = (Schema) => {
return `\
## ${Schema.id}
${Schema.schema.description || NO_DESCRIPTION}
#### Details
* **Type** - ${typeOrLink(Schema.schema)}${
Schema.schema.pattern
? `
* **Pattern** - ${quoteOrNa(Schema.schema.pattern)}`
: ''
}
* [**Source Code**](${links.makeCodeLink(Schema.id)})
${makePropertiesSection(Schema)}
${makeExampleSection(Schema)}
${makeAntiExampleSection(Schema)}
`.trim();
};
// Generate the final markdown.
const buildDocs = (InitSchema) => {
const schemas = collectSchemas(InitSchema);
const markdownSections = _.chain(schemas)
.values()
.sortBy('id')
.map(makeMarkdownSection)
.join('\n\n-----\n\n');
const docs = `\
<!-- {% raw %} -->
# \`zapier-platform-schema\` Generated Documentation
This is automatically generated by the \`npm run docs\` command in \`zapier-platform-schema\` version ${quoteOrNa(
packageJson.version,
)}.
To see the docs for a different version, switch to the corresponding version tag in the GitHub repository.
For example, to view docs for version 17.7.0, switch to the tag 'zapier-platform-schema@17.7.0'.
Alternatively, modify the URL directly: https://github.com/zapier/zapier-platform/blob/zapier-platform-schema@17.7.0/packages/schema/docs/build/schema.md, replacing 17.7.0 with your desired version.
-----
## Index
<!-- toc -->
-----
${markdownSections}
<!-- {% endraw %} -->
`.trim();
return toc.insert(docs, { maxdepth: 2, bullets: '*' });
};
module.exports = buildDocs;

View file

@ -0,0 +1,23 @@
'use strict';
const _ = require('lodash');
const packageJson = require('../../package.json');
const exportSchema = (InitSchema) => {
const exportedSchema = {
version: packageJson.version,
schemas: {},
};
const addAndRecurse = (Schema) => {
exportedSchema.schemas[Schema.id.replace('/', '')] = _.omit(
Schema.schema,
'examples',
'antiExamples',
);
Schema.dependencies.map(addAndRecurse);
};
addAndRecurse(InitSchema);
return exportedSchema;
};
module.exports = exportSchema;

View file

@ -0,0 +1,27 @@
const _ = require('lodash');
const packageJson = require('../../package.json');
const constants = require('../constants.js');
// From '</SomeSchema>' to 'SomeSchema'.
const filename = (val) => _.trim(String(val), '/<>');
// From '/SomeSchema' to '#someschema'.
const anchor = (val) => '#' + filename(val.toLowerCase());
const makeCodeLink = (id) =>
`${constants.ROOT_GITHUB}/blob/zapier-platform-schema@${
packageJson.version
}/packages/schema/lib/schemas/${filename(id)}.js`;
const makeDocLink = (id) =>
_.template(constants.DOC_URL_TEMPLATE)({
version: packageJson.version,
anchor: anchor(id),
});
module.exports = {
filename,
anchor,
makeCodeLink,
makeDocLink,
};

View file

@ -0,0 +1,27 @@
'use strict';
const _ = require('lodash');
const makeValidator = require('./makeValidator');
const getRawSchema = (schema) => schema.schema;
const getDependencies = (schema) => schema.dependencies;
const flattenDependencies = (schemas) => {
schemas = schemas || [];
return _.flatten(schemas.map(getDependencies).concat(schemas));
};
const makeSchema = (schemaDefinition, schemaDependencies) => {
const dependencies = flattenDependencies(schemaDependencies);
const validatorDependencies = dependencies.map(getRawSchema);
return {
dependencies,
id: schemaDefinition.id,
schema: schemaDefinition,
validate: makeValidator(schemaDefinition, validatorDependencies).validate,
};
};
module.exports = makeSchema;

View file

@ -0,0 +1,151 @@
'use strict';
const jsonschema = require('jsonschema');
const links = require('./links');
const functionalConstraints = require('../functional-constraints');
const { flattenDeep, get } = require('lodash');
const ambiguousTypes = ['anyOf', 'oneOf', 'allOf'];
const makeLinks = (error, makerFunc) => {
if (typeof error.schema === 'string') {
return [makerFunc(error.schema)];
}
if (
ambiguousTypes.includes(error.name) &&
error.argument &&
error.argument.length
) {
// no way to know what the subschema was, so don't create links for it
return error.argument
.map((s) => (s.includes('subschema') ? '' : makerFunc(s)))
.filter(Boolean);
}
return [];
};
const removeFirstAndLastChar = (s) => s.slice(1, -1);
// always return a string
const makePath = (path, newSegment) =>
(path ? [path, newSegment].join('.') : newSegment) || '';
const processBaseError = (err, path) => {
const completePath = makePath(path, err.property)
.replace(/\.instance\.?/g, '.')
.replace(/\.instance$/, '')
.replace(/\.$/, ''); // Remove any trailing dots
const subSchemas = err.message.match(/\[subschema \d+\]/g);
if (subSchemas) {
subSchemas.forEach((subschema, idx) => {
// err.schema is either an anonymous schema object or the name of a named schema
if (typeof err.schema === 'string') {
// this is basically only for FieldChoicesSchema and I'm not sure why
err.message += ' Consult the docs below for valid subschemas.';
} else {
// the subschemas have a type property
err.message = err.message.replace(
subschema,
err.schema[err.name][idx].type || 'unknown',
);
}
});
}
err.property = completePath;
return err;
};
/**
* We have a lot of `anyOf` schemas that return ambiguous errors. This recurses down the schema until it finds the errors that cause the failures replaces the ambiguity.
* @param {ValidationError} validationError an individual error
* @param {string} path current path in the error chain
* @param {Validator} validator validator object to pass around that has all the schemas
* @param {object} definition the original schema we're defining
*/
const cleanError = (validationError, path, validator, definition) => {
if (ambiguousTypes.includes(validationError.name)) {
// flatObjectSchema requires each property to be a type. instead of recursing down, it's more valuable to say "hey, it's not of these types"
if (validationError.argument.every((s) => s.includes('subschema'))) {
return processBaseError(validationError, path);
}
// Try against each of A, B, and C to take a guess as to which it's closed to
// errorGroups will be an array of arrays of errors
const errorGroups = validationError.argument.map((schemaName, idx) => {
// this is what we'll validate against next
let nextSchema;
// schemaName is either "[subschema n]" or "/NamedSchema"
if (schemaName.startsWith('[subschema')) {
const maybeNamedSchema = validator.schemas[validationError.schema];
if (maybeNamedSchema) {
nextSchema = maybeNamedSchema[validationError.name][idx];
} else {
// hoist the anonymous subschema up
nextSchema = validationError.schema[validationError.name][idx];
}
} else {
nextSchema = validator.schemas[removeFirstAndLastChar(schemaName)];
}
if (validationError.instance === undefined) {
// Work around a jsonschema bug: When the value being validated is
// falsy, validationError.instance isn't available
// See https://github.com/tdegrunt/jsonschema/issues/263
const fullPath =
path.replace(/^instance\./, '') +
validationError.property.replace(/^instance\./, '');
validationError.instance = get(definition, fullPath);
}
const res = validator.validate(validationError.instance, nextSchema);
return res.errors.map((e) =>
cleanError(
e,
makePath(path, validationError.property),
validator,
definition,
),
);
});
// find the group with the fewest errors, that's probably the most accurate
// if we're goign to tweak what gets returned, this is where we'll do it
// a possible improvement could be treating a longer path favorably, like the python implementation does
errorGroups.sort((a, b) => a.length - b.length);
return errorGroups[0];
} else {
// base case
return processBaseError(validationError, path);
}
};
const makeValidator = (mainSchema, subSchemas) => {
const schemas = [mainSchema].concat(subSchemas || []);
const v = new jsonschema.Validator();
schemas.forEach((Schema) => {
v.addSchema(Schema, Schema.id);
});
return {
validate: (definition) => {
const results = v.validate(definition, mainSchema);
const allErrors = results.errors.concat(
functionalConstraints.run(definition, mainSchema),
);
const cleanedErrors = flattenDeep(
allErrors.map((e) => cleanError(e, '', v, definition)),
);
results.errors = cleanedErrors.map((error) => {
error.codeLinks = makeLinks(error, links.makeCodeLink);
error.docLinks = makeLinks(error, links.makeDocLink);
return error;
});
return results;
},
};
};
module.exports = makeValidator;