From 323b9226a3959790376c21cff9d0fa2731446dd8 Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Tue, 6 Mar 2018 14:47:42 -0600 Subject: [PATCH 1/4] move the samples required to a functional constraint --- lib/functional-constraints/index.js | 3 +- lib/functional-constraints/requiredSamples.js | 82 +++++++++++++++++++ lib/schemas/BasicOperationSchema.js | 4 +- lib/schemas/CreateSchema.js | 24 ++++++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 lib/functional-constraints/requiredSamples.js diff --git a/lib/functional-constraints/index.js b/lib/functional-constraints/index.js index b74fc0d..f959958 100644 --- a/lib/functional-constraints/index.js +++ b/lib/functional-constraints/index.js @@ -13,7 +13,8 @@ const checks = [ require('./searchOrCreateKeys'), require('./deepNestedFields'), - require('./mutuallyExclusiveFields') + require('./mutuallyExclusiveFields'), + require('./requiredSamples') ]; const runFunctionalConstraints = definition => { diff --git a/lib/functional-constraints/requiredSamples.js b/lib/functional-constraints/requiredSamples.js new file mode 100644 index 0000000..1127f1c --- /dev/null +++ b/lib/functional-constraints/requiredSamples.js @@ -0,0 +1,82 @@ +'use strict'; + +const _ = require('lodash'); +const jsonschema = require('jsonschema'); + +const validateSearchOrCreateKeys = definition => { + if (!definition.searchOrCreates) { + return []; + } + + const errors = []; + + const searchKeys = _.keys(definition.searches); + const createKeys = _.keys(definition.creates); + + _.each(definition.searchOrCreates, (searchOrCreateDef, key) => { + const searchOrCreateKey = searchOrCreateDef.key; + const searchKey = searchOrCreateDef.search; + const createKey = searchOrCreateDef.create; + + // 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.searchOrCreates.${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.searchOrCreates.${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.searchOrCreates.${key}.create`, + 'invalidKey', + 'create' + ) + ); + } + }); + + return errors; +}; + +module.exports = definition => { + const errors = []; + if (definition.operation && !_.get(definition, 'display.hidden')) { + const samples = _.get(definition, 'operation.sample', {}); + if (!Object.keys(samples).length) { + errors.push( + new jsonschema.ValidationError( + `required "sample" for non-hidden operation`, + definition, + definition.id + ) + ); + } + } + return errors; +}; 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: { From 7cd04d756037f4c0419ce7aff4f17a61a09e999b Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Tue, 6 Mar 2018 15:28:47 -0600 Subject: [PATCH 2/4] add more coverage --- lib/functional-constraints/requiredSamples.js | 61 ------------------- lib/schemas/ResourceMethodCreateSchema.js | 35 +++++++++++ lib/schemas/ResourceMethodGetSchema.js | 42 +++++++++++++ lib/schemas/ResourceMethodHookSchema.js | 39 ++++++++++++ lib/schemas/ResourceMethodListSchema.js | 44 +++++++++++++ lib/schemas/ResourceMethodSearchSchema.js | 35 +++++++++++ lib/schemas/SearchSchema.js | 39 ++++++++++++ lib/schemas/TriggerSchema.js | 41 +++++++++++++ 8 files changed, 275 insertions(+), 61 deletions(-) diff --git a/lib/functional-constraints/requiredSamples.js b/lib/functional-constraints/requiredSamples.js index 1127f1c..919995e 100644 --- a/lib/functional-constraints/requiredSamples.js +++ b/lib/functional-constraints/requiredSamples.js @@ -3,67 +3,6 @@ const _ = require('lodash'); const jsonschema = require('jsonschema'); -const validateSearchOrCreateKeys = definition => { - if (!definition.searchOrCreates) { - return []; - } - - const errors = []; - - const searchKeys = _.keys(definition.searches); - const createKeys = _.keys(definition.creates); - - _.each(definition.searchOrCreates, (searchOrCreateDef, key) => { - const searchOrCreateKey = searchOrCreateDef.key; - const searchKey = searchOrCreateDef.search; - const createKey = searchOrCreateDef.create; - - // 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.searchOrCreates.${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.searchOrCreates.${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.searchOrCreates.${key}.create`, - 'invalidKey', - 'create' - ) - ); - } - }); - - return errors; -}; - module.exports = definition => { const errors = []; if (definition.operation && !_.get(definition, 'display.hidden')) { 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/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.', From 2415bca18c457c27fde9a808eed4af123b80b0f5 Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Thu, 8 Mar 2018 07:04:41 -0600 Subject: [PATCH 3/4] add resource sample testing and inheritance --- lib/functional-constraints/index.js | 4 +- lib/functional-constraints/requiredSamples.js | 52 ++++++-- lib/schemas/ResourceSchema.js | 121 ++++++++++++++++++ lib/utils/makeValidator.js | 2 +- 4 files changed, 166 insertions(+), 13 deletions(-) diff --git a/lib/functional-constraints/index.js b/lib/functional-constraints/index.js index f959958..9072d30 100644 --- a/lib/functional-constraints/index.js +++ b/lib/functional-constraints/index.js @@ -17,9 +17,9 @@ const checks = [ 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 index 919995e..fce994c 100644 --- a/lib/functional-constraints/requiredSamples.js +++ b/lib/functional-constraints/requiredSamples.js @@ -3,19 +3,51 @@ const _ = require('lodash'); const jsonschema = require('jsonschema'); -module.exports = definition => { - const errors = []; - if (definition.operation && !_.get(definition, 'display.hidden')) { - const samples = _.get(definition, 'operation.sample', {}); - if (!Object.keys(samples).length) { - errors.push( +// 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', {}); + if (!Object.keys(samples).length) { + return new jsonschema.ValidationError( + 'required "sample" for non-hidden operation', + 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( - `required "sample" for non-hidden operation`, + 'expected at least one resource operation', definition, definition.id ) - ); - } + ]; + } else { + definitions = [definition]; } - return errors; + + return definitions.map(check).filter(Boolean); }; 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/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); From 13f5418f32319692207991da10067085f7aefa85 Mon Sep 17 00:00:00 2001 From: Olmo Maldonado Date: Fri, 9 Mar 2018 14:31:58 -0600 Subject: [PATCH 4/4] eslint --- lib/functional-constraints/requiredSamples.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/functional-constraints/requiredSamples.js b/lib/functional-constraints/requiredSamples.js index fce994c..5f53a7a 100644 --- a/lib/functional-constraints/requiredSamples.js +++ b/lib/functional-constraints/requiredSamples.js @@ -8,16 +8,18 @@ const RESOURCE_ID = '/ResourceSchema'; const RESOURCE_METHODS = ['get', 'hook', 'list', 'search', 'create']; const check = definition => { - if (!definition.operation || _.get(definition, 'display.hidden')) return null; + if (!definition.operation || _.get(definition, 'display.hidden')) { + return null; + } const samples = _.get(definition, 'operation.sample', {}); - if (!Object.keys(samples).length) { - return new jsonschema.ValidationError( - 'required "sample" for non-hidden operation', - definition, - definition.id - ); - } + return !_.isEmpty(samples) + ? null + : new jsonschema.ValidationError( + 'requires "sample", because it\'s not hidden', + definition, + definition.id + ); }; module.exports = (definition, mainSchema) => { @@ -37,7 +39,7 @@ module.exports = (definition, mainSchema) => { }); } - if (!definitions.length) + if (!definitions.length) { return [ new jsonschema.ValidationError( 'expected at least one resource operation', @@ -45,6 +47,7 @@ module.exports = (definition, mainSchema) => { definition.id ) ]; + } } else { definitions = [definition]; }