diff --git a/lib/functional-constraints/index.js b/lib/functional-constraints/index.js index b74fc0d..9072d30 100644 --- a/lib/functional-constraints/index.js +++ b/lib/functional-constraints/index.js @@ -13,12 +13,13 @@ const checks = [ require('./searchOrCreateKeys'), require('./deepNestedFields'), - require('./mutuallyExclusiveFields') + require('./mutuallyExclusiveFields'), + require('./requiredSamples') ]; -const runFunctionalConstraints = definition => { +const runFunctionalConstraints = (definition, mainSchema) => { return checks.reduce((errors, checkFunc) => { - const errorsForCheck = checkFunc(definition); + const errorsForCheck = checkFunc(definition, mainSchema); if (errorsForCheck) { errors = errors.concat(errorsForCheck); } diff --git a/lib/functional-constraints/requiredSamples.js b/lib/functional-constraints/requiredSamples.js new file mode 100644 index 0000000..5f53a7a --- /dev/null +++ b/lib/functional-constraints/requiredSamples.js @@ -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); +}; diff --git a/lib/schemas/BasicOperationSchema.js b/lib/schemas/BasicOperationSchema.js index a4f8fcf..a8af96e 100644 --- a/lib/schemas/BasicOperationSchema.js +++ b/lib/schemas/BasicOperationSchema.js @@ -14,7 +14,7 @@ module.exports = makeSchema( description: 'Represents the fundamental mechanics of triggers, searches, or creates.', type: 'object', - required: ['perform', 'sample'], + required: ['perform'], properties: { resource: { description: @@ -38,7 +38,7 @@ module.exports = makeSchema( }, sample: { description: - 'What does a sample of data look like? Will use resource sample if missing.', + 'What does a sample of data look like? Will use resource sample if missing. Required, if the display is not hidden.', type: 'object', // TODO: require id, ID, Id property? minProperties: 1 diff --git a/lib/schemas/CreateSchema.js b/lib/schemas/CreateSchema.js index 12544c9..23e439f 100644 --- a/lib/schemas/CreateSchema.js +++ b/lib/schemas/CreateSchema.js @@ -34,6 +34,18 @@ module.exports = makeSchema( sample: { id: 1 }, shouldLock: true } + }, + { + key: 'recipe', + noun: 'Recipe', + display: { + label: 'Create Recipe', + description: 'Creates a new recipe.', + hidden: true + }, + operation: { + perform: '$func$2$f$' + } } ], antiExamples: [ @@ -46,6 +58,18 @@ module.exports = makeSchema( description: 'Creates a new recipe.' }, operation: { perform: '$func$2$f$', shouldLock: 'yes' } + }, + { + key: 'recipe', + noun: 'Recipe', + display: { + label: 'Create Recipe', + description: 'Creates a new recipe.' + }, + operation: { + perform: '$func$2$f$' + // sample is missing! + } } ], properties: { diff --git a/lib/schemas/ResourceMethodCreateSchema.js b/lib/schemas/ResourceMethodCreateSchema.js index b30ba8b..284f6ca 100644 --- a/lib/schemas/ResourceMethodCreateSchema.js +++ b/lib/schemas/ResourceMethodCreateSchema.js @@ -12,6 +12,41 @@ module.exports = makeSchema( 'How will we find create a specific object given inputs? Will be turned into a create automatically.', type: 'object', required: ['display', 'operation'], + 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: [ + { + display: { + label: 'Create Tag', + description: 'Create a new Tag in your account.' + }, + operation: { + perform: '$func$2$f$' + } + } + ], properties: { display: { description: 'Define how this create method will be exposed in the UI.', diff --git a/lib/schemas/ResourceMethodGetSchema.js b/lib/schemas/ResourceMethodGetSchema.js index 383b2e5..6902792 100644 --- a/lib/schemas/ResourceMethodGetSchema.js +++ b/lib/schemas/ResourceMethodGetSchema.js @@ -12,6 +12,48 @@ module.exports = makeSchema( 'How will we get a single object given a unique identifier/id?', type: 'object', required: ['display', 'operation'], + 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: [ + { + display: { + label: 'Get Tag by ID', + description: 'Grab a specific Tag by ID.' + }, + operation: { + perform: { + url: '$func$0$f$' + } + } + } + ], properties: { display: { description: 'Define how this get method will be exposed in the UI.', diff --git a/lib/schemas/ResourceMethodHookSchema.js b/lib/schemas/ResourceMethodHookSchema.js index 744607e..c74811a 100644 --- a/lib/schemas/ResourceMethodHookSchema.js +++ b/lib/schemas/ResourceMethodHookSchema.js @@ -12,6 +12,45 @@ module.exports = makeSchema( 'How will we get notified of new objects? Will be turned into a trigger automatically.', type: 'object', required: ['display', 'operation'], + 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: [ + { + display: { + label: 'Get Tag by ID', + description: 'Grab a specific Tag by ID.' + }, + operation: { + type: 'hook', + perform: '$func$0$f$' + } + } + ], properties: { display: { description: diff --git a/lib/schemas/ResourceMethodListSchema.js b/lib/schemas/ResourceMethodListSchema.js index 9aa2971..f829aea 100644 --- a/lib/schemas/ResourceMethodListSchema.js +++ b/lib/schemas/ResourceMethodListSchema.js @@ -12,6 +12,50 @@ module.exports = makeSchema( 'How will we get a list of new objects? Will be turned into a trigger automatically.', type: 'object', required: ['display', 'operation'], + examples: [ + { + display: { + label: 'New User', + description: 'Trigger when a new User is created in your account.' + }, + operation: { + perform: { + url: 'http://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: 'http://fake-crm.getsandbox.com/users' + } + } + } + ], + antiExamples: [ + { + display: { + label: 'New User', + description: 'Trigger when a new User is created in your account.' + }, + operation: { + perform: { + url: 'http://fake-crm.getsandbox.com/users' + } + // missing sample + } + } + ], properties: { display: { description: diff --git a/lib/schemas/ResourceMethodSearchSchema.js b/lib/schemas/ResourceMethodSearchSchema.js index efd9fc3..09173fb 100644 --- a/lib/schemas/ResourceMethodSearchSchema.js +++ b/lib/schemas/ResourceMethodSearchSchema.js @@ -12,6 +12,41 @@ module.exports = makeSchema( 'How will we find a specific object given filters or search terms? Will be turned into a search automatically.', type: 'object', required: ['display', 'operation'], + 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: [ + { + key: 'recipe', + noun: 'Recipe', + display: { + label: 'Find a Recipe', + description: 'Search for recipe by cuisine style.' + }, + operation: { + perform: '$func$2$f$' + } + } + ], properties: { display: { description: 'Define how this search method will be exposed in the UI.', diff --git a/lib/schemas/ResourceSchema.js b/lib/schemas/ResourceSchema.js index a454d86..2b2c370 100644 --- a/lib/schemas/ResourceSchema.js +++ b/lib/schemas/ResourceSchema.js @@ -17,6 +17,127 @@ module.exports = makeSchema( 'Represents a resource, which will in turn power triggers, searches, or creates.', type: 'object', required: ['key', 'noun'], + examples: [ + { + key: 'tag', + noun: 'Tag', + get: { + display: { + label: 'Get Tag by ID', + description: 'Grab a specific Tag by ID.' + }, + operation: { + perform: { + url: 'http://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: 'http://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: 'http://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: 'http://fake-crm.getsandbox.com/tags' + }, + sample: { + id: 385, + name: 'proactive enable ROI' + } + } + } + } + ], + antiExamples: [ + { + key: 'tag', + noun: 'Tag', + get: { + display: { + label: 'Get Tag by ID', + description: 'Grab a specific Tag by ID.' + }, + operation: { + perform: { + url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' + } + // missing sample (and no sample on resource) + } + }, + list: { + display: { + label: 'New Tag', + description: 'Trigger when a new Tag is created in your account.' + }, + operation: { + perform: { + url: 'http://fake-crm.getsandbox.com/tags' + }, + sample: { + id: 385, + name: 'proactive enable ROI' + } + } + } + }, + { + key: 'tag', + noun: 'Tag', + get: { + display: { + label: 'Get Tag by ID', + description: 'Grab a specific Tag by ID.' + }, + operation: { + perform: { + url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' + } + // missing sample (and no sample on resource) + } + } + } + ], properties: { key: { description: 'A key to uniquely identify this resource.', diff --git a/lib/schemas/SearchSchema.js b/lib/schemas/SearchSchema.js index 859bee4..5a5b2e8 100644 --- a/lib/schemas/SearchSchema.js +++ b/lib/schemas/SearchSchema.js @@ -12,6 +12,45 @@ module.exports = makeSchema( description: 'How will Zapier search for existing objects?', type: 'object', required: ['key', 'noun', 'display', 'operation'], + 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: [ + 'abc', + { + key: 'recipe', + noun: 'Recipe', + display: { + label: 'Find a Recipe', + description: 'Search for recipe by cuisine style.' + }, + operation: { + perform: '$func$2$f$' + // missing sample + } + } + ], properties: { key: { description: 'A key to uniquely identify this search.', diff --git a/lib/schemas/TriggerSchema.js b/lib/schemas/TriggerSchema.js index 710d3d7..78a1a4b 100644 --- a/lib/schemas/TriggerSchema.js +++ b/lib/schemas/TriggerSchema.js @@ -13,6 +13,47 @@ module.exports = makeSchema( description: 'How will Zapier get notified of new objects?', type: 'object', required: ['key', 'noun', 'display', 'operation'], + 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: [ + { + key: 'new_recipe', + noun: 'Recipe', + display: { + label: 'New Recipe', + description: 'Triggers when a new recipe is added.' + }, + operation: { + perform: '$func$0$f$' + } + } + ], properties: { key: { description: 'A key to uniquely identify this trigger.', diff --git a/lib/utils/makeValidator.js b/lib/utils/makeValidator.js index 1765bf7..687c164 100644 --- a/lib/utils/makeValidator.js +++ b/lib/utils/makeValidator.js @@ -28,7 +28,7 @@ const makeValidator = (mainSchema, subSchemas) => { validate: definition => { const results = v.validate(definition, mainSchema); results.errors = results.errors.concat( - functionalConstraints.run(definition) + functionalConstraints.run(definition, mainSchema) ); results.errors = results.errors.map(error => { error.codeLinks = makeLinks(error, links.makeCodeLink);