├── .nvmrc ├── .prettierignore ├── .gitignore ├── CHANGELOG.md ├── smoke-test ├── test-export ├── test-validate └── smoke-test.js ├── .github └── ISSUE_TEMPLATE.md ├── bin ├── export.js └── docs.js ├── lib ├── schemas │ ├── KeySchema.js │ ├── FieldsSchema.js │ ├── ResultsSchema.js │ ├── AuthenticationBasicConfigSchema.js │ ├── AuthenticationDigestConfigSchema.js │ ├── AuthenticationCustomConfigSchema.js │ ├── FunctionRequireSchema.js │ ├── MiddlewaresSchema.js │ ├── RefResourceSchema.js │ ├── BundleSchema.js │ ├── VersionSchema.js │ ├── HydratorsSchema.js │ ├── CreatesSchema.js │ ├── SearchesSchema.js │ ├── TriggersSchema.js │ ├── SearchOrCreatesSchema.js │ ├── DynamicFieldsSchema.js │ ├── AppFlagsSchema.js │ ├── AuthenticationSessionConfigSchema.js │ ├── ResourcesSchema.js │ ├── FieldOrFunctionSchema.js │ ├── BasicCreateActionOperationSchema.js │ ├── FunctionSourceSchema.js │ ├── FieldChoicesSchema.js │ ├── FlatObjectSchema.js │ ├── RedirectRequestSchema.js │ ├── FunctionSchema.js │ ├── SearchOrCreateSchema.js │ ├── FieldChoiceWithLabelSchema.js │ ├── BasicPollingOperationSchema.js │ ├── AuthenticationOAuth1ConfigSchema.js │ ├── BasicActionOperationSchema.js │ ├── AuthenticationOAuth2ConfigSchema.js │ ├── ResourceMethodCreateSchema.js │ ├── ResourceMethodSearchSchema.js │ ├── ResourceMethodGetSchema.js │ ├── BasicDisplaySchema.js │ ├── ResourceMethodHookSchema.js │ ├── ResourceMethodListSchema.js │ ├── SearchSchema.js │ ├── BasicOperationSchema.js │ ├── TriggerSchema.js │ ├── CreateSchema.js │ ├── RequestSchema.js │ ├── BasicHookOperationSchema.js │ ├── AuthenticationSchema.js │ ├── AppSchema.js │ ├── FieldSchema.js │ └── ResourceSchema.js ├── utils │ ├── exportSchema.js │ ├── links.js │ ├── makeSchema.js │ ├── makeValidator.js │ └── buildDocs.js ├── constants.js └── functional-constraints │ ├── matchingKeys.js │ ├── index.js │ ├── requiredSamples.js │ ├── mutuallyExclusiveFields.js │ ├── deepNestedFields.js │ └── searchOrCreateKeys.js ├── schema.js ├── .travis.yml ├── docs └── build │ └── schema.html ├── README.md ├── test ├── utils.js ├── functional-constraints │ ├── matchingKeys.js │ ├── deepNestedFields.js │ └── mutuallyExclusiveFields.js ├── readability.js └── index.js ├── package.json ├── .eslintrc ├── examples └── definition.json └── exported-schema.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.10.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.zip 3 | *.log 4 | coverage 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Read docs: https://zapier.github.io/zapier-platform-cli/. 2 | 3 | Changelog: https://github.com/zapier/zapier-platform-cli/blob/master/CHANGELOG.md 4 | -------------------------------------------------------------------------------- /smoke-test/test-export: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const zapierSchema = require('zapier-platform-schema'); 4 | console.log(JSON.stringify(zapierSchema.exportSchema())); 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please open your issues on https://github.com/zapier/zapier-platform-cli/issues instead. 2 | 3 | Also, we have a Slack room at https://zapier-platform-slack.herokuapp.com/ set up if you have any other questions. 4 | -------------------------------------------------------------------------------- /bin/export.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const schema = require('../schema'); 6 | const exportedSchema = schema.exportSchema(); 7 | 8 | fs.writeFileSync( 9 | './exported-schema.json', 10 | JSON.stringify(exportedSchema, null, ' ') 11 | ); 12 | -------------------------------------------------------------------------------- /lib/schemas/KeySchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/KeySchema', 7 | description: 'A unique identifier for this item.', 8 | type: 'string', 9 | minLength: 2, 10 | pattern: '^[a-zA-Z]+[a-zA-Z0-9_]*$' 11 | }); 12 | -------------------------------------------------------------------------------- /bin/docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const constants = require('../lib/constants'); 6 | 7 | const AppSchema = require('../lib/schemas/AppSchema'); 8 | const buildDocs = require('../lib/utils/buildDocs'); 9 | 10 | fs.writeFileSync(`./${constants.DOCS_PATH}`, buildDocs(AppSchema)); 11 | -------------------------------------------------------------------------------- /schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const exportSchema = require('./lib/utils/exportSchema'); 4 | 5 | const AppSchema = require('./lib/schemas/AppSchema'); 6 | 7 | const validateAppDefinition = AppSchema.validate; 8 | 9 | module.exports = { 10 | AppSchema, 11 | validateAppDefinition, 12 | exportSchema: () => exportSchema(AppSchema) 13 | }; 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.10.0" 4 | - "10" 5 | script: 6 | - npm run coverage 7 | - npm run smoke-test 8 | notifications: 9 | email: false 10 | deploy: 11 | provider: npm 12 | email: engineering@zapier.com 13 | api_key: $NPM_TOKEN 14 | on: 15 | tags: true 16 | node: "8.10.0" 17 | skip_cleanup: true 18 | -------------------------------------------------------------------------------- /lib/schemas/FieldsSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FieldSchema = require('./FieldSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/FieldsSchema', 10 | description: 'An array or collection of fields.', 11 | type: 'array', 12 | items: { $ref: FieldSchema.id } 13 | }, 14 | [FieldSchema] 15 | ); 16 | -------------------------------------------------------------------------------- /lib/schemas/ResultsSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/ResultsSchema', 7 | description: 'An array of objects suitable for returning in perform calls.', 8 | type: 'array', 9 | items: { 10 | type: 'object', 11 | // TODO: require id, ID, Id property? 12 | minProperties: 1 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationBasicConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/AuthenticationBasicConfigSchema', 7 | description: 8 | '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.', 9 | type: 'object', 10 | properties: {}, 11 | additionalProperties: false 12 | }); 13 | -------------------------------------------------------------------------------- /smoke-test/test-validate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const zapierSchema = require('zapier-platform-schema'); 4 | 5 | const validApp = { 6 | version: '1.0.0', 7 | platformVersion: '7.0.0' 8 | }; 9 | 10 | const invalidApp = {}; 11 | 12 | const testApps = [validApp, invalidApp]; 13 | 14 | const results = testApps.map(app => 15 | zapierSchema.validateAppDefinition(app).errors.map(err => err.message) 16 | ); 17 | 18 | console.log(JSON.stringify(results)); 19 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationDigestConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/AuthenticationDigestConfigSchema', 7 | description: 8 | '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.', 9 | type: 'object', 10 | properties: {}, 11 | additionalProperties: false 12 | }); 13 | -------------------------------------------------------------------------------- /docs/build/schema.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationCustomConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/AuthenticationCustomConfigSchema', 7 | description: 8 | '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.', 9 | type: 'object', 10 | properties: {}, 11 | additionalProperties: false 12 | }); 13 | -------------------------------------------------------------------------------- /lib/schemas/FunctionRequireSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/FunctionRequireSchema', 7 | description: 8 | 'A path to a file that might have content like `module.exports = (z, bundle) => [{id: 123}];`.', 9 | examples: [{ require: 'some/path/to/file.js' }], 10 | type: 'object', 11 | additionalProperties: false, 12 | required: ['require'], 13 | properties: { 14 | require: { type: 'string' } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /lib/schemas/MiddlewaresSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | const FunctionSchema = require('./FunctionSchema'); 5 | 6 | module.exports = makeSchema({ 7 | id: '/MiddlewaresSchema', 8 | description: 9 | 'List of before or after middlewares. Can be an array of functions or a single function', 10 | oneOf: [ 11 | { 12 | type: 'array', 13 | items: { $ref: FunctionSchema.id } 14 | }, 15 | { $ref: FunctionSchema.id } 16 | ], 17 | additionalProperties: false 18 | }); 19 | -------------------------------------------------------------------------------- /lib/schemas/RefResourceSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/RefResourceSchema', 7 | description: 8 | 'Reference a resource by key and the data it returns. In the format of: `{resource_key}.{foreign_key}(.{human_label_key})`.', 9 | type: 'string', 10 | examples: ['contact', 'contact.id', 'contact.id.full_name'], 11 | antiExamples: ['Contact.list.id.full_name'], 12 | pattern: '^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)?(\\.[a-zA-Z0-9_]+)?$' 13 | }); 14 | -------------------------------------------------------------------------------- /lib/schemas/BundleSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/BundleSchema', 7 | description: 'Given as the "arguments" or input to a perform call.', 8 | type: 'object', 9 | examples: [{}, { authData: {}, inputData: {}, inputDataRaw: {} }], 10 | antiExamples: [{ random: true }], 11 | properties: { 12 | authData: { type: 'object' }, 13 | inputData: { type: 'object' }, 14 | inputDataRaw: { type: 'object' } 15 | }, 16 | additionalProperties: false 17 | }); 18 | -------------------------------------------------------------------------------- /lib/schemas/VersionSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/VersionSchema', 7 | description: 8 | 'Represents a simplified semver string, from `0.0.0` to `999.999.999`.', 9 | examples: ['1.0.0', '2.11.3', '999.999.999'], 10 | antiExamples: [ 11 | '1.0.0.0', 12 | '1000.0.0', 13 | 'v1.0.0', 14 | '1.0.0-beta', 15 | '1.0.0-beta.x.y.z', 16 | '1.0.0-beta+12487' 17 | ], 18 | type: 'string', 19 | pattern: '^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$' 20 | }); 21 | -------------------------------------------------------------------------------- /lib/schemas/HydratorsSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | const FunctionSchema = require('./FunctionSchema'); 5 | 6 | module.exports = makeSchema({ 7 | id: '/HydratorsSchema', 8 | description: 9 | "A bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.", 10 | type: 'object', 11 | patternProperties: { 12 | '^[a-zA-Z]+[a-zA-Z0-9]*$': { 13 | description: 14 | "Any unique key can be used in `z.hydrate('uniqueKeyHere')`.", 15 | $ref: FunctionSchema.id 16 | } 17 | }, 18 | additionalProperties: false 19 | }); 20 | -------------------------------------------------------------------------------- /lib/schemas/CreatesSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const CreateSchema = require('./CreateSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/CreatesSchema', 10 | description: 'Enumerates the creates your app has available for users.', 11 | type: 'object', 12 | patternProperties: { 13 | '^[a-zA-Z]+[a-zA-Z0-9_]*$': { 14 | description: 15 | 'Any unique key can be used and its values will be validated against the CreateSchema.', 16 | $ref: CreateSchema.id 17 | } 18 | } 19 | }, 20 | [CreateSchema] 21 | ); 22 | -------------------------------------------------------------------------------- /lib/schemas/SearchesSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const SearchSchema = require('./SearchSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/SearchesSchema', 10 | description: 'Enumerates the searches your app has available for users.', 11 | type: 'object', 12 | patternProperties: { 13 | '^[a-zA-Z]+[a-zA-Z0-9_]*$': { 14 | description: 15 | 'Any unique key can be used and its values will be validated against the SearchSchema.', 16 | $ref: SearchSchema.id 17 | } 18 | } 19 | }, 20 | [SearchSchema] 21 | ); 22 | -------------------------------------------------------------------------------- /lib/utils/exportSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const packageJson = require('../../package.json'); 5 | 6 | const exportSchema = InitSchema => { 7 | const exportedSchema = { 8 | version: packageJson.version, 9 | schemas: {} 10 | }; 11 | const addAndRecurse = Schema => { 12 | exportedSchema.schemas[Schema.id.replace('/', '')] = _.omit( 13 | Schema.schema, 14 | 'examples', 15 | 'antiExamples' 16 | ); 17 | Schema.dependencies.map(addAndRecurse); 18 | }; 19 | addAndRecurse(InitSchema); 20 | return exportedSchema; 21 | }; 22 | 23 | module.exports = exportSchema; 24 | -------------------------------------------------------------------------------- /lib/schemas/TriggersSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const TriggerSchema = require('./TriggerSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/TriggersSchema', 10 | description: 'Enumerates the triggers your app has available for users.', 11 | type: 'object', 12 | patternProperties: { 13 | '^[a-zA-Z]+[a-zA-Z0-9_]*$': { 14 | description: 15 | 'Any unique key can be used and its values will be validated against the TriggerSchema.', 16 | $ref: TriggerSchema.id 17 | } 18 | }, 19 | additionalProperties: false 20 | }, 21 | [TriggerSchema] 22 | ); 23 | -------------------------------------------------------------------------------- /lib/schemas/SearchOrCreatesSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const SearchOrCreateSchema = require('./SearchOrCreateSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/SearchOrCreatesSchema', 10 | description: 11 | 'Enumerates the search-or-creates your app has available for users.', 12 | type: 'object', 13 | patternProperties: { 14 | '^[a-zA-Z]+[a-zA-Z0-9_]*$': { 15 | description: 16 | 'Any unique key can be used and its values will be validated against the SearchOrCreateSchema.', 17 | $ref: SearchOrCreateSchema.id 18 | } 19 | } 20 | }, 21 | [SearchOrCreateSchema] 22 | ); 23 | -------------------------------------------------------------------------------- /lib/schemas/DynamicFieldsSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FieldOrFunctionSchema = require('./FieldOrFunctionSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/DynamicFieldsSchema', 10 | description: 11 | 'Like [/FieldsSchema](#fieldsschema) but you can provide functions to create dynamic or custom fields.', 12 | examples: [ 13 | [], 14 | [{ key: 'abc' }], 15 | [{ key: 'abc' }, '$func$2$f$'], 16 | ['$func$2$f$', '$func$2$f$'] 17 | ], 18 | antiExamples: [[{}], [{ key: 'abc', choices: {} }], '$func$2$f$'], 19 | $ref: FieldOrFunctionSchema.id 20 | }, 21 | [FieldOrFunctionSchema] 22 | ); 23 | -------------------------------------------------------------------------------- /lib/schemas/AppFlagsSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/AppFlagsSchema', 7 | description: 'Codifies high-level options for your app.', 8 | type: 'object', 9 | properties: { 10 | skipHttpPatch: { 11 | description: 12 | "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).", 13 | type: 'boolean' 14 | } 15 | }, 16 | additionalProperties: false, 17 | examples: [{ skipHttpPatch: true }, { skipHttpPatch: false }, {}], 18 | antiExamples: [{ blah: true }, { skipHttpPatch: 'yes' }] 19 | }); 20 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationSessionConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const RequestSchema = require('./RequestSchema'); 6 | const FunctionSchema = require('./FunctionSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/AuthenticationSessionConfigSchema', 11 | description: 'Config for session authentication.', 12 | type: 'object', 13 | required: ['perform'], 14 | properties: { 15 | perform: { 16 | description: 17 | 'Define how Zapier fetches the additional authData needed to make API calls.', 18 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 19 | } 20 | }, 21 | additionalProperties: false 22 | }, 23 | [FunctionSchema, RequestSchema] 24 | ); 25 | -------------------------------------------------------------------------------- /lib/schemas/ResourcesSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | const ResourceSchema = require('./ResourceSchema'); 5 | 6 | module.exports = makeSchema( 7 | { 8 | id: '/ResourcesSchema', 9 | description: 10 | '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.', 11 | type: 'object', 12 | patternProperties: { 13 | '^[a-zA-Z]+[a-zA-Z0-9_]*$': { 14 | description: 15 | 'Any unique key can be used and its values will be validated against the ResourceSchema.', 16 | $ref: ResourceSchema.id 17 | } 18 | }, 19 | additionalProperties: false 20 | }, 21 | [ResourceSchema] 22 | ); 23 | -------------------------------------------------------------------------------- /lib/utils/links.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const packageJson = require('../../package.json'); 4 | const constants = require('../constants.js'); 5 | 6 | // From '' to 'SomeSchema'. 7 | const filename = val => _.trim(String(val), '/<>'); 8 | 9 | // From '/SomeSchema' to '#someschema'. 10 | const anchor = val => '#' + filename(val.toLowerCase()); 11 | 12 | const makeCodeLink = id => 13 | `${constants.ROOT_GITHUB}/blob/v${packageJson.version}/lib/schemas/${filename( 14 | id 15 | )}.js`; 16 | const makeDocLink = id => 17 | `${constants.ROOT_GITHUB}/blob/v${packageJson.version}/${ 18 | constants.DOCS_PATH 19 | }${anchor(id)}`; 20 | 21 | module.exports = { 22 | filename: filename, 23 | anchor: anchor, 24 | makeCodeLink: makeCodeLink, 25 | makeDocLink: makeDocLink 26 | }; 27 | -------------------------------------------------------------------------------- /lib/schemas/FieldOrFunctionSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FieldSchema = require('./FieldSchema'); 6 | const FunctionSchema = require('./FunctionSchema'); 7 | 8 | // This schema was created to improve readability on errors. 9 | 10 | module.exports = makeSchema( 11 | { 12 | id: '/FieldOrFunctionSchema', 13 | description: 'Represents an array of fields or functions.', 14 | examples: [ 15 | [], 16 | [{ key: 'abc' }], 17 | [{ key: 'abc' }, '$func$2$f$'], 18 | ['$func$2$f$', '$func$2$f$'] 19 | ], 20 | antiExamples: [[{}], [{ key: 'abc', choices: {} }], '$func$2$f$'], 21 | type: 'array', 22 | items: { 23 | oneOf: [{ $ref: FieldSchema.id }, { $ref: FunctionSchema.id }] 24 | } 25 | }, 26 | [FieldSchema, FunctionSchema] 27 | ); 28 | -------------------------------------------------------------------------------- /lib/utils/makeSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const makeValidator = require('./makeValidator'); 6 | 7 | const getRawSchema = schema => schema.schema; 8 | 9 | const getDependencies = schema => schema.dependencies; 10 | 11 | const flattenDependencies = schemas => { 12 | schemas = schemas || []; 13 | return _.flatten(schemas.map(getDependencies).concat(schemas)); 14 | }; 15 | 16 | const makeSchema = (schemaDefinition, schemaDependencies) => { 17 | const dependencies = flattenDependencies(schemaDependencies); 18 | const validatorDependencies = dependencies.map(getRawSchema); 19 | return { 20 | dependencies, 21 | id: schemaDefinition.id, 22 | schema: schemaDefinition, 23 | validate: makeValidator(schemaDefinition, validatorDependencies).validate 24 | }; 25 | }; 26 | 27 | module.exports = makeSchema; 28 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ROOT_GITHUB: 'https://github.com/zapier/zapier-platform-schema', 3 | DOCS_PATH: 'docs/build/schema.md', 4 | SKIP_KEY: '_skipTest', 5 | // the following pairs of keys can't be used together in FieldSchema 6 | // they're stored here because they're used in a few places 7 | INCOMPATIBLE_FIELD_SCHEMA_KEYS: [ 8 | ['children', 'list'], // This is actually a Feature Request (https://github.com/zapier/zapier-platform-cli/issues/115) 9 | ['children', 'dict'], // dict is ignored 10 | ['children', 'type'], // type is ignored 11 | ['children', 'placeholder'], // placeholder is ignored 12 | ['children', 'helpText'], // helpText is ignored 13 | ['children', 'default'], // default is ignored 14 | ['dict', 'list'], // Use only one or the other 15 | ['dynamic', 'dict'], // dict is ignored 16 | ['dynamic', 'choices'] // choices are ignored 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MOVED 2 | 3 | This code has been moved to: https://github.com/zapier/zapier-platform/tree/master/packages/schema 4 | 5 | --- 6 | 7 | # Schema For Zapier CLI Platform 8 | 9 | [Visit the CLI for basic documentation and instructions on how to use](https://zapier.github.io/zapier-platform-cli). 10 | 11 | [View all the schema definitions](https://zapier.github.io/zapier-platform-schema/build/schema.html). 12 | 13 | ## Development 14 | 15 | - `npm install` for getting started 16 | - `npm test` for running tests 17 | - `npm run export` for updating the exported-schema (even if only the version changes) 18 | - `npm run docs` for updating docs (even if only the version changes) 19 | - `npm run coverage` for running tests and displaying test coverage 20 | 21 | ## Publishing (after merging) 22 | 23 | - `npm version [patch|minor|major]` will pull, test, update exported-schema, update docs, increment version in package.json, push tags, and publish to npm 24 | -------------------------------------------------------------------------------- /lib/schemas/BasicCreateActionOperationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicActionOperationSchema = require('./BasicActionOperationSchema'); 6 | 7 | // TODO: would be nice to deep merge these instead 8 | // or maybe use allOf which is built into json-schema 9 | const BasicCreateActionOperationSchema = JSON.parse( 10 | JSON.stringify(BasicActionOperationSchema.schema) 11 | ); 12 | 13 | BasicCreateActionOperationSchema.id = '/BasicCreateActionOperationSchema'; 14 | BasicCreateActionOperationSchema.description = 15 | 'Represents the fundamental mechanics of a create.'; 16 | 17 | BasicCreateActionOperationSchema.properties.shouldLock = { 18 | description: 19 | 'Should this action be performed one at a time (avoid concurrency)?', 20 | type: 'boolean' 21 | }; 22 | 23 | module.exports = makeSchema( 24 | BasicCreateActionOperationSchema, 25 | BasicActionOperationSchema.dependencies 26 | ); 27 | -------------------------------------------------------------------------------- /lib/schemas/FunctionSourceSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/FunctionSourceSchema', 7 | description: 8 | 'Source code like `{source: "return 1 + 2"}` which the system will wrap in a function for you.', 9 | examples: [ 10 | { source: 'return 1 + 2' }, 11 | { args: ['x', 'y'], source: 'return x + y;' } 12 | ], 13 | antiExamples: [{ source: '1 + 2' }], 14 | type: 'object', 15 | additionalProperties: false, 16 | required: ['source'], 17 | properties: { 18 | source: { 19 | type: 'string', 20 | pattern: 'return', 21 | description: 22 | 'JavaScript code for the function body. This must end with a `return` statement.' 23 | }, 24 | args: { 25 | type: 'array', 26 | items: { type: 'string' }, 27 | description: 28 | "Function signature. Defaults to `['z', 'bundle']` if not specified." 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /lib/functional-constraints/matchingKeys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const jsonschema = require('jsonschema'); 5 | 6 | const actionTypes = ['triggers', 'searches', 'creates']; 7 | 8 | const matchingKeys = definition => { 9 | const errors = []; 10 | 11 | // verifies that x.key === x 12 | // otherwise, we double results in the compiled app via core's compileApp 13 | 14 | for (const actionType of actionTypes) { 15 | const group = definition[actionType] || {}; 16 | _.each(group, (action, key) => { 17 | if (action.key !== key) { 18 | errors.push( 19 | new jsonschema.ValidationError( 20 | `must have a matching top-level key (found "${key}" and "${ 21 | action.key 22 | }")`, 23 | action, 24 | `/${_.capitalize(actionType)}Schema`, 25 | `instance.${key}.key`, 26 | 'invalid', 27 | 'key' 28 | ) 29 | ); 30 | } 31 | }); 32 | } 33 | 34 | return errors; 35 | }; 36 | 37 | module.exports = matchingKeys; 38 | -------------------------------------------------------------------------------- /lib/schemas/FieldChoicesSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FieldChoiceWithLabelSchema = require('./FieldChoiceWithLabelSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/FieldChoicesSchema', 10 | description: 11 | '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)', 12 | examples: [{ a: '1', b: '2', c: '3' }, ['first', 'second', 'third']], 13 | antiExamples: [[1, 2, 3], [{ a: '1', b: '2', c: '3' }]], 14 | oneOf: [ 15 | { 16 | type: 'object', 17 | minProperties: 1 18 | }, 19 | { 20 | type: 'array', 21 | minItems: 1, 22 | items: { 23 | oneOf: [{ type: 'string' }, { $ref: FieldChoiceWithLabelSchema.id }] 24 | } 25 | } 26 | ] 27 | }, 28 | [FieldChoiceWithLabelSchema] 29 | ); 30 | -------------------------------------------------------------------------------- /lib/schemas/FlatObjectSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/FlatObjectSchema', 7 | description: 'An object whose values can only be primitives', 8 | type: 'object', 9 | examples: [ 10 | { a: 1, b: 2, c: 3 }, 11 | { a: 1.2, b: 2.2, c: 3.3 }, 12 | { a: 'a', b: 'b', c: 'c' }, 13 | { a: true, b: true, c: false }, 14 | { a: 'a', b: 2, c: 3.1, d: true, e: false }, 15 | { 123: 'hello' } 16 | ], 17 | antiExamples: [ 18 | { a: {}, b: 2 }, 19 | { a: { aa: 1 }, b: 2 }, 20 | { a: [], b: 2 }, 21 | { a: [1, 2, 3], b: 2 }, 22 | { '': 1 }, 23 | { ' ': 1 }, 24 | { ' ': 1 } 25 | ], 26 | patternProperties: { 27 | '[^\\s]+': { 28 | description: 29 | 'Any key may exist in this flat object as long as its values are simple.', 30 | anyOf: [ 31 | { type: 'null' }, 32 | { type: 'string' }, 33 | { type: 'integer' }, 34 | { type: 'number' }, 35 | { type: 'boolean' } 36 | ] 37 | } 38 | }, 39 | additionalProperties: false 40 | }); 41 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | const { SKIP_KEY } = require('../lib/constants'); 5 | 6 | const testInlineSchemaExamples = name => { 7 | const Schema = require('../lib/schemas/' + name); 8 | const goods = Schema.schema.examples || []; 9 | const bads = Schema.schema.antiExamples || []; 10 | 11 | describe(name, () => { 12 | it('valid example schemas should pass validation', function() { 13 | if (!goods.length) { 14 | this.skip(); 15 | } else { 16 | goods.filter(t => !t[SKIP_KEY]).forEach(good => { 17 | const errors = Schema.validate(good).errors; 18 | errors.should.have.length(0); 19 | }); 20 | } 21 | }); 22 | 23 | it('invalid example schemas should fail validation', function() { 24 | if (!bads.length) { 25 | this.skip(); 26 | } else { 27 | bads.filter(t => !t[SKIP_KEY]).forEach(bad => { 28 | const errors = Schema.validate(bad).errors; 29 | errors.should.not.have.length(0); 30 | }); 31 | } 32 | }); 33 | }); 34 | }; 35 | 36 | module.exports = { 37 | testInlineSchemaExamples 38 | }; 39 | -------------------------------------------------------------------------------- /lib/schemas/RedirectRequestSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FlatObjectSchema = require('./FlatObjectSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/RedirectRequestSchema', 10 | description: 11 | 'A representation of a HTTP redirect - you can use the `{{syntax}}` to inject authentication, field or global variables.', 12 | type: 'object', 13 | properties: { 14 | method: { 15 | description: 'The HTTP method for the request.', 16 | type: 'string', 17 | default: 'GET', 18 | enum: ['GET'] 19 | }, 20 | url: { 21 | description: 22 | 'A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.', 23 | type: 'string' 24 | }, 25 | params: { 26 | description: 27 | 'A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.', 28 | $ref: FlatObjectSchema.id 29 | } 30 | }, 31 | additionalProperties: false 32 | }, 33 | [FlatObjectSchema] 34 | ); 35 | -------------------------------------------------------------------------------- /lib/schemas/FunctionSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FunctionRequireSchema = require('./FunctionRequireSchema'); 6 | const FunctionSourceSchema = require('./FunctionSourceSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/FunctionSchema', 11 | description: 12 | '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.', 13 | examples: [ 14 | '$func$0$f$', 15 | '$func$2$t$', 16 | { source: 'return 1 + 2' }, 17 | { require: 'some/path/to/file.js' } 18 | ], 19 | antiExamples: [ 20 | 'funcy', 21 | { source: '1 + 2' }, 22 | { source: '1 + 2', require: 'some/path/to/file.js' } 23 | ], 24 | oneOf: [ 25 | { type: 'string', pattern: '^\\$func\\$\\d+\\$[tf]\\$$' }, 26 | { $ref: FunctionRequireSchema.id }, 27 | { $ref: FunctionSourceSchema.id } 28 | ] 29 | }, 30 | [FunctionRequireSchema, FunctionSourceSchema] 31 | ); 32 | -------------------------------------------------------------------------------- /lib/schemas/SearchOrCreateSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const KeySchema = require('./KeySchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/SearchOrCreateSchema', 11 | description: 12 | 'Pair an existing search and a create to enable "Find or Create" functionality in your app', 13 | type: 'object', 14 | required: ['key', 'display', 'search', 'create'], 15 | properties: { 16 | key: { 17 | description: 18 | 'A key to uniquely identify this search-or-create. Must match the search key.', 19 | $ref: KeySchema.id 20 | }, 21 | display: { 22 | description: 'Configures the UI for this search-or-create.', 23 | $ref: BasicDisplaySchema.id 24 | }, 25 | search: { 26 | description: 'The key of the search that powers this search-or-create', 27 | $ref: KeySchema.id 28 | }, 29 | create: { 30 | description: 'The key of the create that powers this search-or-create', 31 | $ref: KeySchema.id 32 | } 33 | }, 34 | additionalProperties: false 35 | }, 36 | [BasicDisplaySchema, KeySchema] 37 | ); 38 | -------------------------------------------------------------------------------- /test/functional-constraints/matchingKeys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | const schema = require('../../schema'); 5 | 6 | describe('matchingKeys', () => { 7 | it("should error if the keys don't match", () => { 8 | const definition = { 9 | version: '1.0.0', 10 | platformVersion: '1.0.0', 11 | triggers: {}, // this should be harmlessly skipped 12 | creates: { 13 | foo: { 14 | key: 'bar', // this is different than above, which shouldn't validate 15 | noun: 'Foo', 16 | display: { 17 | label: 'Create Foo', 18 | description: 'Creates a...' 19 | }, 20 | operation: { 21 | perform: '$func$2$f$', 22 | sample: { id: 1 }, 23 | inputFields: [ 24 | { key: 'orderId', type: 'number' }, 25 | { 26 | key: 'line_items', 27 | children: [ 28 | { 29 | key: 'product', 30 | type: 'string' 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | } 37 | } 38 | }; 39 | 40 | const results = schema.validateAppDefinition(definition); 41 | results.errors.should.have.length(1); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /lib/functional-constraints/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Each check below is expected to return a list of ValidationSchema errors. An error is defined by: 4 | * new jsonschema.ValidationError( 5 | * message, // string that explains the problem, like 'must not have a URL that points to AWS' 6 | * instance, // the snippet of the app defintion that is invalid 7 | * schema, // name of the schema that failed, like '/TriggerSchema' 8 | * propertyPath, // stringified path to problematic snippet, like 'instance.triggers.find_contact' 9 | * name, // optional, the validation type that failed. Can make something up like 'invalidUrl' 10 | * argument // optional 11 | * ); 12 | */ 13 | const checks = [ 14 | require('./searchOrCreateKeys'), 15 | require('./deepNestedFields'), 16 | require('./mutuallyExclusiveFields'), 17 | require('./requiredSamples'), 18 | require('./matchingKeys') 19 | ]; 20 | 21 | const runFunctionalConstraints = (definition, mainSchema) => { 22 | return checks.reduce((errors, checkFunc) => { 23 | const errorsForCheck = checkFunc(definition, mainSchema); 24 | if (errorsForCheck) { 25 | errors = errors.concat(errorsForCheck); 26 | } 27 | return errors; 28 | }, []); 29 | }; 30 | 31 | module.exports = { 32 | run: runFunctionalConstraints 33 | }; 34 | -------------------------------------------------------------------------------- /lib/schemas/FieldChoiceWithLabelSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/FieldChoiceWithLabelSchema', 7 | description: 8 | "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)).", 9 | examples: [{ label: 'Red', sample: '#f00', value: '#f00' }], 10 | antiExamples: [{ label: 'Red', value: '#f00' }], 11 | type: 'object', 12 | required: ['value', 'sample', 'label'], 13 | properties: { 14 | value: { 15 | description: 16 | 'The actual value that is sent into the Zap. Should match sample exactly.', 17 | type: 'string', 18 | minLength: 1 19 | }, 20 | sample: { 21 | description: 22 | "Displayed as light grey text in the editor. It's important that the value match the sample. Otherwise, the actual value won't match what the user picked, which is confusing.", 23 | type: 'string', 24 | minLength: 1 25 | }, 26 | label: { 27 | description: 'A human readable label for this value.', 28 | type: 'string', 29 | minLength: 1 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /lib/schemas/BasicPollingOperationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicOperationSchema = require('./BasicOperationSchema'); 6 | 7 | // TODO: would be nice to deep merge these instead 8 | // or maybe use allOf which is built into json-schema 9 | const BasicPollingOperationSchema = JSON.parse( 10 | JSON.stringify(BasicOperationSchema.schema) 11 | ); 12 | 13 | BasicPollingOperationSchema.id = '/BasicPollingOperationSchema'; 14 | BasicPollingOperationSchema.description = 15 | 'Represents the fundamental mechanics of a trigger.'; 16 | 17 | BasicPollingOperationSchema.properties = { 18 | type: { 19 | // TODO: not a fan of this... 20 | description: 21 | 'Clarify how this operation works (polling == pull or hook == push).', 22 | type: 'string', 23 | default: 'polling', 24 | enum: ['polling'] // notification? 25 | }, 26 | resource: BasicPollingOperationSchema.properties.resource, 27 | perform: BasicPollingOperationSchema.properties.perform, 28 | canPaginate: { 29 | description: 'Does this endpoint support a page offset?', 30 | type: 'boolean' 31 | }, 32 | inputFields: BasicPollingOperationSchema.properties.inputFields, 33 | outputFields: BasicPollingOperationSchema.properties.outputFields, 34 | sample: BasicPollingOperationSchema.properties.sample 35 | }; 36 | 37 | module.exports = makeSchema( 38 | BasicPollingOperationSchema, 39 | BasicOperationSchema.dependencies 40 | ); 41 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationOAuth1ConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FunctionSchema = require('./FunctionSchema'); 6 | const RedirectRequestSchema = require('./RedirectRequestSchema'); 7 | const RequestSchema = require('./RequestSchema'); 8 | 9 | module.exports = makeSchema( 10 | { 11 | id: '/AuthenticationOAuth1ConfigSchema', 12 | description: 'Config for OAuth1 authentication.', 13 | type: 'object', 14 | required: ['getRequestToken', 'authorizeUrl', 'getAccessToken'], 15 | properties: { 16 | getRequestToken: { 17 | description: 18 | 'Define where Zapier will acquire a request token which is used for the rest of the three legged authentication process.', 19 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 20 | }, 21 | authorizeUrl: { 22 | description: 23 | 'Define where Zapier will redirect the user to authorize our app. Typically, you should append an `oauth_token` querystring parameter to the request.', 24 | oneOf: [{ $ref: RedirectRequestSchema.id }, { $ref: FunctionSchema.id }] 25 | }, 26 | getAccessToken: { 27 | description: 'Define how Zapier fetches an access token from the API', 28 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 29 | } 30 | }, 31 | additionalProperties: false 32 | }, 33 | [FunctionSchema, RedirectRequestSchema, RequestSchema] 34 | ); 35 | -------------------------------------------------------------------------------- /lib/schemas/BasicActionOperationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicOperationSchema = require('./BasicOperationSchema'); 6 | const FunctionSchema = require('./FunctionSchema'); 7 | const RequestSchema = require('./RequestSchema'); 8 | 9 | // TODO: would be nice to deep merge these instead 10 | // or maybe use allOf which is built into json-schema 11 | const BasicActionOperationSchema = JSON.parse( 12 | JSON.stringify(BasicOperationSchema.schema) 13 | ); 14 | 15 | BasicActionOperationSchema.id = '/BasicActionOperationSchema'; 16 | BasicActionOperationSchema.description = 17 | 'Represents the fundamental mechanics of a search/create.'; 18 | 19 | BasicActionOperationSchema.properties = { 20 | resource: BasicActionOperationSchema.properties.resource, 21 | perform: BasicActionOperationSchema.properties.perform, 22 | performResume: { 23 | description: 24 | 'A function that parses data from a perform + callback to resume this action. For use with callback semantics', 25 | $ref: FunctionSchema.id 26 | }, 27 | performGet: { 28 | description: 29 | 'How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.', 30 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 31 | }, 32 | inputFields: BasicActionOperationSchema.properties.inputFields, 33 | outputFields: BasicActionOperationSchema.properties.outputFields, 34 | sample: BasicActionOperationSchema.properties.sample 35 | }; 36 | 37 | module.exports = makeSchema( 38 | BasicActionOperationSchema, 39 | BasicOperationSchema.dependencies 40 | ); 41 | -------------------------------------------------------------------------------- /lib/functional-constraints/requiredSamples.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const jsonschema = require('jsonschema'); 5 | 6 | // todo: deal with circular dep. 7 | const RESOURCE_ID = '/ResourceSchema'; 8 | const RESOURCE_METHODS = ['get', 'hook', 'list', 'search', 'create']; 9 | 10 | const check = definition => { 11 | if (!definition.operation || _.get(definition, 'display.hidden')) { 12 | return null; 13 | } 14 | 15 | const samples = _.get(definition, 'operation.sample', {}); 16 | return !_.isEmpty(samples) 17 | ? null 18 | : new jsonschema.ValidationError( 19 | 'requires "sample", because it\'s not hidden', 20 | definition, 21 | definition.id 22 | ); 23 | }; 24 | 25 | module.exports = (definition, mainSchema) => { 26 | let definitions = []; 27 | 28 | if (mainSchema.id === RESOURCE_ID) { 29 | definitions = RESOURCE_METHODS.map(method => definition[method]).filter( 30 | Boolean 31 | ); 32 | 33 | // allow method definitions to inherit the sample 34 | if (definition.sample) { 35 | definitions.forEach(methodDefinition => { 36 | if (methodDefinition.operation && !methodDefinition.operation.sample) { 37 | methodDefinition.operation.sample = definition.sample; 38 | } 39 | }); 40 | } 41 | 42 | if (!definitions.length) { 43 | return [ 44 | new jsonschema.ValidationError( 45 | 'expected at least one resource operation', 46 | definition, 47 | definition.id 48 | ) 49 | ]; 50 | } 51 | } else { 52 | definitions = [definition]; 53 | } 54 | 55 | return definitions.map(check).filter(Boolean); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationOAuth2ConfigSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FunctionSchema = require('./FunctionSchema'); 6 | const RedirectRequestSchema = require('./RedirectRequestSchema'); 7 | const RequestSchema = require('./RequestSchema'); 8 | 9 | module.exports = makeSchema( 10 | { 11 | id: '/AuthenticationOAuth2ConfigSchema', 12 | description: 'Config for OAuth2 authentication.', 13 | type: 'object', 14 | required: ['authorizeUrl', 'getAccessToken'], 15 | properties: { 16 | authorizeUrl: { 17 | description: 18 | '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.', 19 | oneOf: [{ $ref: RedirectRequestSchema.id }, { $ref: FunctionSchema.id }] 20 | }, 21 | getAccessToken: { 22 | description: 'Define how Zapier fetches an access token from the API', 23 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 24 | }, 25 | refreshAccessToken: { 26 | description: 27 | 'Define how Zapier will refresh the access token from the API', 28 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 29 | }, 30 | scope: { 31 | description: 'What scope should Zapier request?', 32 | type: 'string' 33 | }, 34 | autoRefresh: { 35 | description: 36 | 'Should Zapier include a pre-built afterResponse middleware that invokes `refreshAccessToken` when we receive a 401 response?', 37 | type: 'boolean' 38 | } 39 | }, 40 | additionalProperties: false 41 | }, 42 | [FunctionSchema, RedirectRequestSchema, RequestSchema] 43 | ); 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zapier-platform-schema", 3 | "version": "8.2.1", 4 | "description": "Schema definition for CLI apps in the Zapier Developer Platform.", 5 | "repository": "zapier/zapier-platform-schema", 6 | "homepage": "https://zapier.com/", 7 | "author": "Bryan Helmig ", 8 | "license": "UNLICENSED", 9 | "main": "schema.js", 10 | "files": [ 11 | "/exported-schema.json", 12 | "/lib/**/*.js", 13 | "/schema.js" 14 | ], 15 | "scripts": { 16 | "preversion": "git pull && npm test", 17 | "version": "npm run build && npm run add", 18 | "postversion": "git push && git push --tags", 19 | "test": "mocha -t 5000 --recursive test", 20 | "test:debug": "mocha -t 5000 --recursive --inspect-brk test", 21 | "posttest": "eslint lib", 22 | "smoke-test": "mocha -t 5000 --recursive smoke-test", 23 | "coverage": "istanbul cover _mocha -- --recursive", 24 | "export": "node bin/export.js && prettier --write exported-schema.json", 25 | "docs": "node bin/docs.js", 26 | "build": "npm run docs && npm run export", 27 | "add": "git add exported-schema.json README.md docs", 28 | "precommit": "npm run build && npm run add && lint-staged" 29 | }, 30 | "dependencies": { 31 | "jsonschema": "1.1.1", 32 | "lodash": "4.17.11" 33 | }, 34 | "devDependencies": { 35 | "eslint": "4.19.1", 36 | "fs-extra": "7.0.0", 37 | "husky": "0.14.3", 38 | "istanbul": "0.4.5", 39 | "lint-staged": "^6.0.0", 40 | "markdown-toc": "1.2.0", 41 | "mocha": "5.1.1", 42 | "node-fetch": "2.2.0", 43 | "prettier": "1.9.2", 44 | "should": "11.1.0" 45 | }, 46 | "prettier": { 47 | "singleQuote": true 48 | }, 49 | "lint-staged": { 50 | "*.{js,json}": [ 51 | "prettier --write", 52 | "git add" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/schemas/ResourceMethodCreateSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicActionOperationSchema = require('./BasicActionOperationSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/ResourceMethodCreateSchema', 11 | description: 12 | 'How will we find create a specific object given inputs? Will be turned into a create automatically.', 13 | type: 'object', 14 | required: ['display', 'operation'], 15 | examples: [ 16 | { 17 | display: { 18 | label: 'Create Tag', 19 | description: 'Create a new Tag in your account.' 20 | }, 21 | operation: { 22 | perform: '$func$2$f$', 23 | sample: { 24 | id: 1 25 | } 26 | } 27 | }, 28 | { 29 | display: { 30 | label: 'Create Tag', 31 | description: 'Create a new Tag in your account.', 32 | hidden: true 33 | }, 34 | operation: { 35 | perform: '$func$2$f$' 36 | } 37 | } 38 | ], 39 | antiExamples: [ 40 | { 41 | display: { 42 | label: 'Create Tag', 43 | description: 'Create a new Tag in your account.' 44 | }, 45 | operation: { 46 | perform: '$func$2$f$' 47 | } 48 | } 49 | ], 50 | properties: { 51 | display: { 52 | description: 'Define how this create method will be exposed in the UI.', 53 | $ref: BasicDisplaySchema.id 54 | }, 55 | operation: { 56 | description: 'Define how this create method will work.', 57 | $ref: BasicActionOperationSchema.id 58 | } 59 | }, 60 | additionalProperties: false 61 | }, 62 | [BasicDisplaySchema, BasicActionOperationSchema] 63 | ); 64 | -------------------------------------------------------------------------------- /lib/functional-constraints/mutuallyExclusiveFields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const jsonschema = require('jsonschema'); 5 | 6 | // NOTE: While it would be possible to accomplish this with a solution like 7 | // https://stackoverflow.com/questions/28162509/mutually-exclusive-property-groups#28172831 8 | // it was harder to read and understand. 9 | 10 | const { INCOMPATIBLE_FIELD_SCHEMA_KEYS } = require('../constants'); 11 | 12 | const verifyIncompatibilities = (inputFields, path) => { 13 | const errors = []; 14 | 15 | _.each(inputFields, (inputField, index) => { 16 | _.each(INCOMPATIBLE_FIELD_SCHEMA_KEYS, ([firstField, secondField]) => { 17 | if (_.has(inputField, firstField) && _.has(inputField, secondField)) { 18 | errors.push( 19 | new jsonschema.ValidationError( 20 | `must not contain ${firstField} and ${secondField}, as they're mutually exclusive.`, 21 | inputField, 22 | '/FieldSchema', 23 | `instance.${path}.inputFields[${index}]`, 24 | 'invalid', 25 | 'inputFields' 26 | ) 27 | ); 28 | } 29 | }); 30 | }); 31 | 32 | return errors; 33 | }; 34 | 35 | const mutuallyExclusiveFields = definition => { 36 | let errors = []; 37 | 38 | _.each(['triggers', 'searches', 'creates'], typeOf => { 39 | if (definition[typeOf]) { 40 | _.each(definition[typeOf], actionDef => { 41 | if (actionDef.operation && actionDef.operation.inputFields) { 42 | errors = [ 43 | ...errors, 44 | ...verifyIncompatibilities( 45 | actionDef.operation.inputFields, 46 | `${typeOf}.${actionDef.key}` 47 | ) 48 | ]; 49 | } 50 | }); 51 | } 52 | }); 53 | 54 | return errors; 55 | }; 56 | 57 | module.exports = mutuallyExclusiveFields; 58 | -------------------------------------------------------------------------------- /lib/schemas/ResourceMethodSearchSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicActionOperationSchema = require('./BasicActionOperationSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/ResourceMethodSearchSchema', 11 | description: 12 | 'How will we find a specific object given filters or search terms? Will be turned into a search automatically.', 13 | type: 'object', 14 | required: ['display', 'operation'], 15 | examples: [ 16 | { 17 | display: { 18 | label: 'Find a Recipe', 19 | description: 'Search for recipe by cuisine style.' 20 | }, 21 | operation: { 22 | perform: '$func$2$f$', 23 | sample: { id: 1 } 24 | } 25 | }, 26 | { 27 | display: { 28 | label: 'Find a Recipe', 29 | description: 'Search for recipe by cuisine style.', 30 | hidden: true 31 | }, 32 | operation: { 33 | perform: '$func$2$f$' 34 | } 35 | } 36 | ], 37 | antiExamples: [ 38 | { 39 | key: 'recipe', 40 | noun: 'Recipe', 41 | display: { 42 | label: 'Find a Recipe', 43 | description: 'Search for recipe by cuisine style.' 44 | }, 45 | operation: { 46 | perform: '$func$2$f$' 47 | } 48 | } 49 | ], 50 | properties: { 51 | display: { 52 | description: 'Define how this search method will be exposed in the UI.', 53 | $ref: BasicDisplaySchema.id 54 | }, 55 | operation: { 56 | description: 'Define how this search method will work.', 57 | $ref: BasicActionOperationSchema.id 58 | } 59 | }, 60 | additionalProperties: false 61 | }, 62 | [BasicDisplaySchema, BasicActionOperationSchema] 63 | ); 64 | -------------------------------------------------------------------------------- /lib/schemas/ResourceMethodGetSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicOperationSchema = require('./BasicOperationSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/ResourceMethodGetSchema', 11 | description: 12 | 'How will we get a single object given a unique identifier/id?', 13 | type: 'object', 14 | required: ['display', 'operation'], 15 | examples: [ 16 | { 17 | display: { 18 | label: 'Get Tag by ID', 19 | description: 'Grab a specific Tag by ID.' 20 | }, 21 | operation: { 22 | perform: { 23 | url: '$func$0$f$' 24 | }, 25 | sample: { 26 | id: 385, 27 | name: 'proactive enable ROI' 28 | } 29 | } 30 | }, 31 | { 32 | display: { 33 | label: 'Get Tag by ID', 34 | description: 'Grab a specific Tag by ID.', 35 | hidden: true 36 | }, 37 | operation: { 38 | perform: { 39 | url: '$func$0$f$' 40 | } 41 | } 42 | } 43 | ], 44 | antiExamples: [ 45 | { 46 | display: { 47 | label: 'Get Tag by ID', 48 | description: 'Grab a specific Tag by ID.' 49 | }, 50 | operation: { 51 | perform: { 52 | url: '$func$0$f$' 53 | } 54 | } 55 | } 56 | ], 57 | properties: { 58 | display: { 59 | description: 'Define how this get method will be exposed in the UI.', 60 | $ref: BasicDisplaySchema.id 61 | }, 62 | operation: { 63 | description: 'Define how this get method will work.', 64 | $ref: BasicOperationSchema.id 65 | } 66 | }, 67 | additionalProperties: false 68 | }, 69 | [BasicDisplaySchema, BasicOperationSchema] 70 | ); 71 | -------------------------------------------------------------------------------- /lib/schemas/BasicDisplaySchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | module.exports = makeSchema({ 6 | id: '/BasicDisplaySchema', 7 | description: 'Represents user information for a trigger, search, or create.', 8 | type: 'object', 9 | examples: [ 10 | { label: 'New Thing', description: 'Gets a new thing for you.' }, 11 | { 12 | label: 'New Thing', 13 | description: 'Gets a new thing for you.', 14 | directions: 'This is how you use the thing.', 15 | hidden: false, 16 | important: true 17 | } 18 | ], 19 | antiExamples: [ 20 | { label: 'New Thing' }, 21 | { 22 | label: 'New Thing', 23 | description: 'Gets a new thing for you.', 24 | important: 1 25 | } 26 | ], 27 | required: ['label', 'description'], 28 | properties: { 29 | label: { 30 | description: 31 | 'A short label like "New Record" or "Create Record in Project".', 32 | type: 'string', 33 | minLength: 2, 34 | maxLength: 64 35 | }, 36 | description: { 37 | description: 38 | 'A description of what this trigger, search, or create does.', 39 | type: 'string', 40 | minLength: 1, 41 | maxLength: 1000 42 | }, 43 | directions: { 44 | description: 45 | '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.', 46 | type: 'string', 47 | minLength: 12, 48 | maxLength: 1000 49 | }, 50 | important: { 51 | description: 52 | 'Affects how prominently this operation is displayed in the UI. Only mark a few of the most popular operations important.', 53 | type: 'boolean' 54 | }, 55 | hidden: { 56 | description: 'Should this operation be unselectable by users?', 57 | type: 'boolean' 58 | } 59 | }, 60 | additionalProperties: false 61 | }); 62 | -------------------------------------------------------------------------------- /lib/functional-constraints/deepNestedFields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const jsonschema = require('jsonschema'); 5 | 6 | const collectErrors = (inputFields, path) => { 7 | const errors = []; 8 | 9 | _.each(inputFields, (inputField, index) => { 10 | if (inputField.children) { 11 | if (inputField.children.length === 0) { 12 | errors.push( 13 | new jsonschema.ValidationError( 14 | 'must not be empty.', 15 | inputField, 16 | '/FieldSchema', 17 | `instance.${path}.inputFields[${index}].children`, 18 | 'empty', 19 | 'inputFields' 20 | ) 21 | ); 22 | } else { 23 | const hasDeeplyNestedChildren = _.some( 24 | inputField.children, 25 | child => child.children 26 | ); 27 | 28 | if (hasDeeplyNestedChildren) { 29 | errors.push( 30 | new jsonschema.ValidationError( 31 | 'must not contain deeply nested child fields. One level max.', 32 | inputField, 33 | '/FieldSchema', 34 | `instance.${path}.inputFields[${index}]`, 35 | 'deepNesting', 36 | 'inputFields' 37 | ) 38 | ); 39 | } 40 | } 41 | } 42 | }); 43 | 44 | return errors; 45 | }; 46 | 47 | const validateFieldNesting = definition => { 48 | let errors = []; 49 | 50 | _.each(['triggers', 'searches', 'creates'], typeOf => { 51 | if (definition[typeOf]) { 52 | _.each(definition[typeOf], actionDef => { 53 | if (actionDef.operation && actionDef.operation.inputFields) { 54 | errors = errors.concat( 55 | collectErrors( 56 | actionDef.operation.inputFields, 57 | `${typeOf}.${actionDef.key}` 58 | ) 59 | ); 60 | } 61 | }); 62 | } 63 | }); 64 | 65 | return errors; 66 | }; 67 | 68 | module.exports = validateFieldNesting; 69 | -------------------------------------------------------------------------------- /lib/schemas/ResourceMethodHookSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicHookOperationSchema = require('./BasicHookOperationSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/ResourceMethodHookSchema', 11 | description: 12 | 'How will we get notified of new objects? Will be turned into a trigger automatically.', 13 | type: 'object', 14 | required: ['display', 'operation'], 15 | examples: [ 16 | { 17 | display: { 18 | label: 'Get Tag by ID', 19 | description: 'Grab a specific Tag by ID.' 20 | }, 21 | operation: { 22 | type: 'hook', 23 | perform: '$func$0$f$', 24 | sample: { 25 | id: 385, 26 | name: 'proactive enable ROI' 27 | } 28 | } 29 | }, 30 | { 31 | display: { 32 | label: 'Get Tag by ID', 33 | description: 'Grab a specific Tag by ID.', 34 | hidden: true 35 | }, 36 | operation: { 37 | type: 'hook', 38 | perform: '$func$0$f$' 39 | } 40 | } 41 | ], 42 | antiExamples: [ 43 | { 44 | display: { 45 | label: 'Get Tag by ID', 46 | description: 'Grab a specific Tag by ID.' 47 | }, 48 | operation: { 49 | type: 'hook', 50 | perform: '$func$0$f$' 51 | } 52 | } 53 | ], 54 | properties: { 55 | display: { 56 | description: 57 | 'Define how this hook/trigger method will be exposed in the UI.', 58 | $ref: BasicDisplaySchema.id 59 | }, 60 | operation: { 61 | description: 'Define how this hook/trigger method will work.', 62 | $ref: BasicHookOperationSchema.id 63 | } 64 | }, 65 | additionalProperties: false 66 | }, 67 | [BasicDisplaySchema, BasicHookOperationSchema] 68 | ); 69 | -------------------------------------------------------------------------------- /lib/functional-constraints/searchOrCreateKeys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const jsonschema = require('jsonschema'); 5 | 6 | const validateSearchOrCreateKeys = definition => { 7 | if (!definition.searchOrCreates) { 8 | return []; 9 | } 10 | 11 | const errors = []; 12 | 13 | const searchKeys = _.keys(definition.searches); 14 | const createKeys = _.keys(definition.creates); 15 | 16 | _.each(definition.searchOrCreates, (searchOrCreateDef, key) => { 17 | const searchOrCreateKey = searchOrCreateDef.key; 18 | const searchKey = searchOrCreateDef.search; 19 | const createKey = searchOrCreateDef.create; 20 | 21 | // Confirm searchOrCreate.key matches a searches.key (current Zapier editor limitation) 22 | if (!definition.searches[searchOrCreateKey]) { 23 | errors.push( 24 | new jsonschema.ValidationError( 25 | `must match a "key" from a search (options: ${searchKeys})`, 26 | searchOrCreateDef, 27 | '/SearchOrCreateSchema', 28 | `instance.searchOrCreates.${key}.key`, 29 | 'invalidKey', 30 | 'key' 31 | ) 32 | ); 33 | } 34 | 35 | // Confirm searchOrCreate.search matches a searches.key 36 | if (!definition.searches[searchKey]) { 37 | errors.push( 38 | new jsonschema.ValidationError( 39 | `must match a "key" from a search (options: ${searchKeys})`, 40 | searchOrCreateDef, 41 | '/SearchOrCreateSchema', 42 | `instance.searchOrCreates.${key}.search`, 43 | 'invalidKey', 44 | 'search' 45 | ) 46 | ); 47 | } 48 | 49 | // Confirm searchOrCreate.create matches a creates.key 50 | if (!definition.creates[createKey]) { 51 | errors.push( 52 | new jsonschema.ValidationError( 53 | `must match a "key" from a create (options: ${createKeys})`, 54 | searchOrCreateDef, 55 | '/SearchOrCreateSchema', 56 | `instance.searchOrCreates.${key}.create`, 57 | 'invalidKey', 58 | 'create' 59 | ) 60 | ); 61 | } 62 | }); 63 | 64 | return errors; 65 | }; 66 | 67 | module.exports = validateSearchOrCreateKeys; 68 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 2018 5 | }, 6 | "env": { 7 | "node": true, 8 | "es6": true, 9 | "mocha": true 10 | }, 11 | "globals": { 12 | "Promise": true 13 | }, 14 | "rules": { 15 | "quotes": [2, "single", "avoid-escape"], 16 | "strict": [0, "never"], 17 | "camelcase": 0, 18 | "no-underscore-dangle": 0, 19 | "new-cap": 0, 20 | "comma-dangle": 0, 21 | "comma-spacing": 2, 22 | "consistent-return": 2, 23 | "curly": [2, "all"], 24 | "dot-notation": [2, { "allowKeywords": true }], 25 | "eol-last": 2, 26 | "eqeqeq": [2, "smart"], 27 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 28 | "new-parens": 2, 29 | "no-alert": 2, 30 | "no-array-constructor": 2, 31 | "no-caller": 2, 32 | "no-console": 0, 33 | "no-delete-var": 2, 34 | "no-labels": 2, 35 | "no-eval": 2, 36 | "no-extend-native": 2, 37 | "no-extra-bind": 2, 38 | "no-fallthrough": 2, 39 | "no-implied-eval": 2, 40 | "no-iterator": 2, 41 | "no-label-var": 2, 42 | "no-lone-blocks": 2, 43 | "no-loop-func": 2, 44 | "no-mixed-spaces-and-tabs": [2, false], 45 | "no-multi-spaces": 2, 46 | "no-multi-str": 2, 47 | "no-native-reassign": 2, 48 | "no-new": 2, 49 | "no-new-func": 2, 50 | "no-new-object": 2, 51 | "no-new-wrappers": 2, 52 | "no-octal": 2, 53 | "no-octal-escape": 2, 54 | "no-process-exit": 2, 55 | "no-proto": 2, 56 | "no-redeclare": 2, 57 | "no-return-assign": 2, 58 | "no-script-url": 2, 59 | "no-sequences": 2, 60 | "no-shadow": 2, 61 | "no-shadow-restricted-names": 2, 62 | "no-spaced-func": 2, 63 | "no-trailing-spaces": 2, 64 | "no-undef": 2, 65 | "no-undef-init": 2, 66 | "no-unused-expressions": 2, 67 | "no-unused-vars": [2, { "vars": "all", "args": "after-used" }], 68 | "no-use-before-define": 2, 69 | "no-with": 2, 70 | "semi": 2, 71 | "semi-spacing": [2, { "before": false, "after": true }], 72 | "space-infix-ops": 2, 73 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 74 | "yoda": [2, "never"] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/schemas/ResourceMethodListSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicPollingOperationSchema = require('./BasicPollingOperationSchema'); 7 | 8 | module.exports = makeSchema( 9 | { 10 | id: '/ResourceMethodListSchema', 11 | description: 12 | 'How will we get a list of new objects? Will be turned into a trigger automatically.', 13 | type: 'object', 14 | required: ['display', 'operation'], 15 | examples: [ 16 | { 17 | display: { 18 | label: 'New User', 19 | description: 'Trigger when a new User is created in your account.' 20 | }, 21 | operation: { 22 | perform: { 23 | url: 'http://fake-crm.getsandbox.com/users' 24 | }, 25 | sample: { 26 | id: 49, 27 | name: 'Veronica Kuhn', 28 | email: 'veronica.kuhn@company.com' 29 | } 30 | } 31 | }, 32 | { 33 | display: { 34 | label: 'New User', 35 | description: 'Trigger when a new User is created in your account.', 36 | hidden: true 37 | }, 38 | operation: { 39 | perform: { 40 | url: 'http://fake-crm.getsandbox.com/users' 41 | } 42 | } 43 | } 44 | ], 45 | antiExamples: [ 46 | { 47 | display: { 48 | label: 'New User', 49 | description: 'Trigger when a new User is created in your account.' 50 | }, 51 | operation: { 52 | perform: { 53 | url: 'http://fake-crm.getsandbox.com/users' 54 | } 55 | // missing sample 56 | } 57 | } 58 | ], 59 | properties: { 60 | display: { 61 | description: 62 | 'Define how this list/trigger method will be exposed in the UI.', 63 | $ref: BasicDisplaySchema.id 64 | }, 65 | operation: { 66 | description: 'Define how this list/trigger method will work.', 67 | $ref: BasicPollingOperationSchema.id 68 | } 69 | }, 70 | additionalProperties: false 71 | }, 72 | [BasicDisplaySchema, BasicPollingOperationSchema] 73 | ); 74 | -------------------------------------------------------------------------------- /lib/schemas/SearchSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicActionOperationSchema = require('./BasicActionOperationSchema'); 7 | const KeySchema = require('./KeySchema'); 8 | 9 | module.exports = makeSchema( 10 | { 11 | id: '/SearchSchema', 12 | description: 'How will Zapier search for existing objects?', 13 | type: 'object', 14 | required: ['key', 'noun', 'display', 'operation'], 15 | examples: [ 16 | { 17 | key: 'recipe', 18 | noun: 'Recipe', 19 | display: { 20 | label: 'Find a Recipe', 21 | description: 'Search for recipe by cuisine style.' 22 | }, 23 | operation: { 24 | perform: '$func$2$f$', 25 | sample: { id: 1 } 26 | } 27 | }, 28 | { 29 | key: 'recipe', 30 | noun: 'Recipe', 31 | display: { 32 | label: 'Find a Recipe', 33 | description: 'Search for recipe by cuisine style.', 34 | hidden: true 35 | }, 36 | operation: { perform: '$func$2$f$' } 37 | } 38 | ], 39 | antiExamples: [ 40 | 'abc', 41 | { 42 | key: 'recipe', 43 | noun: 'Recipe', 44 | display: { 45 | label: 'Find a Recipe', 46 | description: 'Search for recipe by cuisine style.' 47 | }, 48 | operation: { 49 | perform: '$func$2$f$' 50 | // missing sample 51 | } 52 | } 53 | ], 54 | properties: { 55 | key: { 56 | description: 'A key to uniquely identify this search.', 57 | $ref: KeySchema.id 58 | }, 59 | noun: { 60 | description: 61 | 'A noun for this search that completes the sentence "finds a specific XXX".', 62 | type: 'string', 63 | minLength: 2, 64 | maxLength: 255 65 | }, 66 | display: { 67 | description: 'Configures the UI for this search.', 68 | $ref: BasicDisplaySchema.id 69 | }, 70 | operation: { 71 | description: 'Powers the functionality for this search.', 72 | $ref: BasicActionOperationSchema.id 73 | } 74 | }, 75 | additionalProperties: false 76 | }, 77 | [BasicDisplaySchema, BasicActionOperationSchema, KeySchema] 78 | ); 79 | -------------------------------------------------------------------------------- /lib/schemas/BasicOperationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const DynamicFieldsSchema = require('./DynamicFieldsSchema'); 6 | const FunctionSchema = require('./FunctionSchema'); 7 | const RefResourceSchema = require('./RefResourceSchema'); 8 | const RequestSchema = require('./RequestSchema'); 9 | const ResultsSchema = require('./ResultsSchema'); 10 | 11 | module.exports = makeSchema( 12 | { 13 | id: '/BasicOperationSchema', 14 | description: 15 | 'Represents the fundamental mechanics of triggers, searches, or creates.', 16 | type: 'object', 17 | required: ['perform'], 18 | properties: { 19 | resource: { 20 | description: 21 | '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.', 22 | $ref: RefResourceSchema.id 23 | }, 24 | perform: { 25 | description: 26 | "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.", 27 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 28 | }, 29 | inputFields: { 30 | description: 31 | 'What should the form a user sees and configures look like?', 32 | $ref: DynamicFieldsSchema.id 33 | }, 34 | outputFields: { 35 | description: 36 | 'What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.', 37 | $ref: DynamicFieldsSchema.id 38 | }, 39 | sample: { 40 | description: 41 | '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', 42 | type: 'object', 43 | // TODO: require id, ID, Id property? 44 | minProperties: 1, 45 | docAnnotation: { 46 | required: { 47 | type: 'replace', // replace or append 48 | value: '**yes** (with exceptions, see description)' 49 | } 50 | } 51 | } 52 | }, 53 | additionalProperties: false 54 | }, 55 | [ 56 | DynamicFieldsSchema, 57 | FunctionSchema, 58 | RefResourceSchema, 59 | RequestSchema, 60 | ResultsSchema 61 | ] 62 | ); 63 | -------------------------------------------------------------------------------- /test/functional-constraints/deepNestedFields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | const schema = require('../../schema'); 5 | 6 | describe('deepNestedFields', () => { 7 | it('should not error on fields nested one level deep', () => { 8 | const definition = { 9 | version: '1.0.0', 10 | platformVersion: '1.0.0', 11 | creates: { 12 | foo: { 13 | key: 'foo', 14 | noun: 'Foo', 15 | display: { 16 | label: 'Create Foo', 17 | description: 'Creates a...' 18 | }, 19 | operation: { 20 | perform: '$func$2$f$', 21 | sample: { id: 1 }, 22 | inputFields: [ 23 | { key: 'orderId', type: 'number' }, 24 | { 25 | key: 'line_items', 26 | children: [ 27 | { 28 | key: 'product', 29 | type: 'string' 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | }; 38 | 39 | const results = schema.validateAppDefinition(definition); 40 | results.errors.should.have.length(0); 41 | }); 42 | 43 | it('should error on fields nested more than one level deep', () => { 44 | const definition = { 45 | version: '1.0.0', 46 | platformVersion: '1.0.0', 47 | creates: { 48 | foo: { 49 | key: 'foo', 50 | noun: 'Foo', 51 | display: { 52 | label: 'Create Foo', 53 | description: 'Creates a...' 54 | }, 55 | operation: { 56 | perform: '$func$2$f$', 57 | sample: { id: 1 }, 58 | inputFields: [ 59 | { key: 'orderId', type: 'number' }, 60 | { 61 | key: 'line_items', 62 | children: [ 63 | { key: 'some do not have children' }, 64 | { 65 | key: 'product', 66 | children: [{ key: 'name', type: 'string' }] 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | } 73 | } 74 | }; 75 | 76 | const results = schema.validateAppDefinition(definition); 77 | results.errors.should.have.length(1); 78 | results.errors[0].stack.should.eql( 79 | 'instance.creates.foo.inputFields[1] must not contain deeply nested child fields. One level max.' 80 | ); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /lib/schemas/TriggerSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicHookOperationSchema = require('./BasicHookOperationSchema'); 7 | const BasicPollingOperationSchema = require('./BasicPollingOperationSchema'); 8 | const KeySchema = require('./KeySchema'); 9 | 10 | module.exports = makeSchema( 11 | { 12 | id: '/TriggerSchema', 13 | description: 'How will Zapier get notified of new objects?', 14 | type: 'object', 15 | required: ['key', 'noun', 'display', 'operation'], 16 | examples: [ 17 | { 18 | key: 'new_recipe', 19 | noun: 'Recipe', 20 | display: { 21 | label: 'New Recipe', 22 | description: 'Triggers when a new recipe is added.' 23 | }, 24 | operation: { 25 | type: 'polling', 26 | perform: '$func$0$f$', 27 | sample: { id: 1 } 28 | } 29 | }, 30 | { 31 | key: 'new_recipe', 32 | noun: 'Recipe', 33 | display: { 34 | label: 'New Recipe', 35 | description: 'Triggers when a new recipe is added.', 36 | hidden: true 37 | }, 38 | operation: { 39 | type: 'polling', 40 | perform: '$func$0$f$' 41 | } 42 | } 43 | ], 44 | antiExamples: [ 45 | { 46 | key: 'new_recipe', 47 | noun: 'Recipe', 48 | display: { 49 | label: 'New Recipe', 50 | description: 'Triggers when a new recipe is added.' 51 | }, 52 | operation: { 53 | perform: '$func$0$f$' 54 | } 55 | } 56 | ], 57 | properties: { 58 | key: { 59 | description: 'A key to uniquely identify this trigger.', 60 | $ref: KeySchema.id 61 | }, 62 | noun: { 63 | description: 64 | 'A noun for this trigger that completes the sentence "triggers on a new XXX".', 65 | type: 'string', 66 | minLength: 2, 67 | maxLength: 255 68 | }, 69 | display: { 70 | description: 'Configures the UI for this trigger.', 71 | $ref: BasicDisplaySchema.id 72 | }, 73 | operation: { 74 | description: 'Powers the functionality for this trigger.', 75 | anyOf: [ 76 | { $ref: BasicPollingOperationSchema.id }, 77 | { $ref: BasicHookOperationSchema.id } 78 | ] 79 | } 80 | }, 81 | additionalProperties: false 82 | }, 83 | [ 84 | KeySchema, 85 | BasicDisplaySchema, 86 | BasicPollingOperationSchema, 87 | BasicHookOperationSchema 88 | ] 89 | ); 90 | -------------------------------------------------------------------------------- /lib/schemas/CreateSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicDisplaySchema = require('./BasicDisplaySchema'); 6 | const BasicCreateActionOperationSchema = require('./BasicCreateActionOperationSchema'); 7 | const KeySchema = require('./KeySchema'); 8 | 9 | module.exports = makeSchema( 10 | { 11 | id: '/CreateSchema', 12 | description: 'How will Zapier create a new object?', 13 | type: 'object', 14 | required: ['key', 'noun', 'display', 'operation'], 15 | examples: [ 16 | { 17 | key: 'recipe', 18 | noun: 'Recipe', 19 | display: { 20 | label: 'Create Recipe', 21 | description: 'Creates a new recipe.' 22 | }, 23 | operation: { perform: '$func$2$f$', sample: { id: 1 } } 24 | }, 25 | { 26 | key: 'recipe', 27 | noun: 'Recipe', 28 | display: { 29 | label: 'Create Recipe', 30 | description: 'Creates a new recipe.' 31 | }, 32 | operation: { 33 | perform: '$func$2$f$', 34 | sample: { id: 1 }, 35 | shouldLock: true 36 | } 37 | }, 38 | { 39 | key: 'recipe', 40 | noun: 'Recipe', 41 | display: { 42 | label: 'Create Recipe', 43 | description: 'Creates a new recipe.', 44 | hidden: true 45 | }, 46 | operation: { 47 | perform: '$func$2$f$' 48 | } 49 | } 50 | ], 51 | antiExamples: [ 52 | 'abc', 53 | { 54 | key: 'recipe', 55 | noun: 'Recipe', 56 | display: { 57 | label: 'Create Recipe', 58 | description: 'Creates a new recipe.' 59 | }, 60 | operation: { perform: '$func$2$f$', shouldLock: 'yes' } 61 | }, 62 | { 63 | key: 'recipe', 64 | noun: 'Recipe', 65 | display: { 66 | label: 'Create Recipe', 67 | description: 'Creates a new recipe.' 68 | }, 69 | operation: { 70 | perform: '$func$2$f$' 71 | // sample is missing! 72 | } 73 | } 74 | ], 75 | properties: { 76 | key: { 77 | description: 'A key to uniquely identify this create.', 78 | $ref: KeySchema.id 79 | }, 80 | noun: { 81 | description: 82 | 'A noun for this create that completes the sentence "creates a new XXX".', 83 | type: 'string', 84 | minLength: 2, 85 | maxLength: 255 86 | }, 87 | display: { 88 | description: 'Configures the UI for this create.', 89 | $ref: BasicDisplaySchema.id 90 | }, 91 | operation: { 92 | description: 'Powers the functionality for this create.', 93 | $ref: BasicCreateActionOperationSchema.id 94 | } 95 | }, 96 | additionalProperties: false 97 | }, 98 | [BasicDisplaySchema, BasicCreateActionOperationSchema, KeySchema] 99 | ); 100 | -------------------------------------------------------------------------------- /lib/schemas/RequestSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const FlatObjectSchema = require('./FlatObjectSchema'); 6 | 7 | module.exports = makeSchema( 8 | { 9 | id: '/RequestSchema', 10 | description: 11 | 'A representation of a HTTP request - you can use the `{{syntax}}` to inject authentication, field or global variables.', 12 | type: 'object', 13 | properties: { 14 | method: { 15 | description: 'The HTTP method for the request.', 16 | type: 'string', 17 | default: 'GET', 18 | enum: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'HEAD'] 19 | }, 20 | url: { 21 | description: 22 | 'A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.', 23 | type: 'string' 24 | }, 25 | body: { 26 | description: 'Can be nothing, a raw string or JSON (object or array).', 27 | oneOf: [ 28 | { type: 'null' }, // nothing 29 | { type: 'string' }, // raw body 30 | { type: 'object' }, // json body object 31 | { type: 'array' } // json body array 32 | ] 33 | }, 34 | params: { 35 | description: 36 | 'A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.', 37 | $ref: FlatObjectSchema.id 38 | }, 39 | headers: { 40 | description: 'The HTTP headers for the request.', 41 | $ref: FlatObjectSchema.id 42 | }, 43 | auth: { 44 | description: 45 | "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']`.", 46 | oneOf: [ 47 | { 48 | type: 'array', 49 | items: { 50 | type: 'string', 51 | minProperties: 2, 52 | maxProperties: 2 53 | } 54 | }, 55 | { $ref: FlatObjectSchema.id } 56 | ] 57 | }, 58 | removeMissingValuesFrom: { 59 | description: 60 | '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 }```', 61 | type: 'object', 62 | properties: { 63 | params: { 64 | description: 65 | 'Refers to data sent via a requests query params (`req.params`)', 66 | type: 'boolean', 67 | default: false 68 | }, 69 | body: { 70 | description: 71 | 'Refers to tokens sent via a requsts body (`req.body`)', 72 | type: 'boolean', 73 | default: false 74 | } 75 | }, 76 | additionalProperties: false 77 | } 78 | }, 79 | additionalProperties: false 80 | }, 81 | [FlatObjectSchema] 82 | ); 83 | -------------------------------------------------------------------------------- /lib/schemas/BasicHookOperationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const BasicOperationSchema = require('./BasicOperationSchema'); 6 | const FunctionSchema = require('./FunctionSchema'); 7 | const RequestSchema = require('./RequestSchema'); 8 | 9 | // TODO: would be nice to deep merge these instead 10 | // or maybe use allOf which is built into json-schema 11 | const BasicHookOperationSchema = JSON.parse( 12 | JSON.stringify(BasicOperationSchema.schema) 13 | ); 14 | 15 | const hookTechnicallyRequired = 16 | 'Note: this is required for public apps to ensure the best UX for the end-user. For private apps, you can ignore warnings about this property with the `--without-style` flag during `zapier push`.'; 17 | 18 | BasicHookOperationSchema.id = '/BasicHookOperationSchema'; 19 | 20 | BasicHookOperationSchema.description = 21 | 'Represents the inbound mechanics of hooks with optional subscribe/unsubscribe. Defers to list for fields.'; 22 | 23 | BasicHookOperationSchema.properties = { 24 | type: { 25 | description: 26 | 'Must be explicitly set to `"hook"` unless this hook is defined as part of a resource, in which case it\'s optional.', 27 | type: 'string', 28 | enum: ['hook'], 29 | docAnnotation: { 30 | required: { 31 | type: 'replace', 32 | value: '**yes** (with exceptions, see description)' 33 | } 34 | } 35 | }, 36 | resource: BasicHookOperationSchema.properties.resource, 37 | perform: { 38 | description: 'A function that processes the inbound webhook request.', 39 | $ref: FunctionSchema.id 40 | }, 41 | performList: { 42 | description: 43 | 'Can get "live" data on demand instead of waiting for a hook. If you find yourself reaching for this - consider resources and their built-in hook/list methods. ' + 44 | hookTechnicallyRequired, 45 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }], 46 | docAnnotation: { 47 | required: { 48 | type: 'replace', 49 | value: '**yes** (with exceptions, see description)' 50 | } 51 | } 52 | }, 53 | performSubscribe: { 54 | description: 55 | 'Takes a URL and any necessary data from the user and subscribes. ' + 56 | hookTechnicallyRequired, 57 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }], 58 | docAnnotation: { 59 | required: { 60 | type: 'replace', 61 | value: '**yes** (with exceptions, see description)' 62 | } 63 | } 64 | }, 65 | performUnsubscribe: { 66 | description: 67 | 'Takes a URL and data from a previous subscribe call and unsubscribes. ' + 68 | hookTechnicallyRequired, 69 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }], 70 | docAnnotation: { 71 | required: { 72 | type: 'replace', 73 | value: '**yes** (with exceptions, see description)' 74 | } 75 | } 76 | }, 77 | inputFields: BasicHookOperationSchema.properties.inputFields, 78 | outputFields: BasicHookOperationSchema.properties.outputFields, 79 | sample: BasicHookOperationSchema.properties.sample 80 | }; 81 | 82 | module.exports = makeSchema( 83 | BasicHookOperationSchema, 84 | BasicOperationSchema.dependencies 85 | ); 86 | -------------------------------------------------------------------------------- /lib/schemas/AuthenticationSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const AuthenticationBasicConfigSchema = require('./AuthenticationBasicConfigSchema.js'); 6 | const AuthenticationCustomConfigSchema = require('./AuthenticationCustomConfigSchema.js'); 7 | const AuthenticationDigestConfigSchema = require('./AuthenticationDigestConfigSchema.js'); 8 | const AuthenticationOAuth1ConfigSchema = require('./AuthenticationOAuth1ConfigSchema.js'); 9 | const AuthenticationOAuth2ConfigSchema = require('./AuthenticationOAuth2ConfigSchema.js'); 10 | const AuthenticationSessionConfigSchema = require('./AuthenticationSessionConfigSchema.js'); 11 | const FieldsSchema = require('./FieldsSchema'); 12 | const FunctionSchema = require('./FunctionSchema'); 13 | const RequestSchema = require('./RequestSchema'); 14 | 15 | module.exports = makeSchema( 16 | { 17 | id: '/AuthenticationSchema', 18 | description: 'Represents authentication schemes.', 19 | examples: [ 20 | { type: 'basic', test: '$func$2$f$' }, 21 | { type: 'custom', test: '$func$2$f$', fields: [{ key: 'abc' }] }, 22 | { 23 | type: 'custom', 24 | test: '$func$2$f$', 25 | connectionLabel: '{{bundle.inputData.abc}}' 26 | }, 27 | { type: 'custom', test: '$func$2$f$', connectionLabel: '$func$2$f$' }, 28 | { type: 'custom', test: '$func$2$f$', connectionLabel: { url: 'abc' } } 29 | ], 30 | antiExamples: [ 31 | {}, 32 | '$func$2$f$', 33 | { type: 'unknown', test: '$func$2$f$' }, 34 | { type: 'custom', test: '$func$2$f$', fields: '$func$2$f$' }, 35 | { 36 | type: 'custom', 37 | test: '$func$2$f$', 38 | fields: [{ key: 'abc' }, '$func$2$f$'] 39 | } 40 | ], 41 | type: 'object', 42 | required: ['type', 'test'], 43 | properties: { 44 | type: { 45 | description: 'Choose which scheme you want to use.', 46 | type: 'string', 47 | enum: ['basic', 'custom', 'digest', 'oauth1', 'oauth2', 'session'] 48 | }, 49 | test: { 50 | description: 51 | 'A function or request that confirms the authentication is working.', 52 | oneOf: [{ $ref: RequestSchema.id }, { $ref: FunctionSchema.id }] 53 | }, 54 | fields: { 55 | description: 56 | 'Fields you can request from the user before they connect your app to Zapier.', 57 | $ref: FieldsSchema.id 58 | }, 59 | connectionLabel: { 60 | description: 61 | 'A string with variables, function, or request that returns the connection label for the authenticated user.', 62 | anyOf: [ 63 | { $ref: RequestSchema.id }, 64 | { $ref: FunctionSchema.id }, 65 | { type: 'string' } 66 | ] 67 | }, 68 | // this is preferred to laying out config: anyOf: [...] 69 | basicConfig: { $ref: AuthenticationBasicConfigSchema.id }, 70 | customConfig: { $ref: AuthenticationCustomConfigSchema.id }, 71 | digestConfig: { $ref: AuthenticationDigestConfigSchema.id }, 72 | oauth1Config: { $ref: AuthenticationOAuth1ConfigSchema.id }, 73 | oauth2Config: { $ref: AuthenticationOAuth2ConfigSchema.id }, 74 | sessionConfig: { $ref: AuthenticationSessionConfigSchema.id } 75 | }, 76 | additionalProperties: false 77 | }, 78 | [ 79 | FieldsSchema, 80 | FunctionSchema, 81 | RequestSchema, 82 | AuthenticationBasicConfigSchema, 83 | AuthenticationCustomConfigSchema, 84 | AuthenticationDigestConfigSchema, 85 | AuthenticationOAuth1ConfigSchema, 86 | AuthenticationOAuth2ConfigSchema, 87 | AuthenticationSessionConfigSchema 88 | ] 89 | ); 90 | -------------------------------------------------------------------------------- /smoke-test/smoke-test.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require('child_process'); 2 | const crypto = require('crypto'); 3 | const fs = require('fs-extra'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | 7 | require('should'); 8 | const fetch = require('node-fetch'); 9 | 10 | const npmPack = () => { 11 | let filename; 12 | const proc = spawnSync('npm', ['pack'], { encoding: 'utf8' }); 13 | const lines = proc.stdout.split('\n'); 14 | for (let i = lines.length - 1; i >= 0; i--) { 15 | const line = lines[i].trim(); 16 | if (line) { 17 | filename = line; 18 | break; 19 | } 20 | } 21 | return filename; 22 | }; 23 | 24 | const setupTempWorkingDir = () => { 25 | let workdir; 26 | const tmpBaseDir = os.tmpdir(); 27 | while (!workdir || fs.existsSync(workdir)) { 28 | workdir = path.join(tmpBaseDir, crypto.randomBytes(20).toString('hex')); 29 | } 30 | fs.mkdirSync(workdir); 31 | return workdir; 32 | }; 33 | 34 | const npmInstall = (packagePath, workdir) => { 35 | spawnSync('npm', ['install', packagePath], { 36 | encoding: 'utf8', 37 | cwd: workdir 38 | }); 39 | }; 40 | 41 | const copyTestScript = (filename, workdir) => { 42 | const dest = path.join(workdir, filename); 43 | fs.copyFileSync(path.join(__dirname, filename), dest); 44 | return dest; 45 | }; 46 | 47 | describe('smoke tests - setup will take some time', () => { 48 | const context = { 49 | // Global context that will be available for all test cases in this test suite 50 | package: { 51 | filename: null, 52 | path: null 53 | }, 54 | workdir: null, 55 | testScripts: { 56 | validate: null, 57 | export: null 58 | } 59 | }; 60 | 61 | before(() => { 62 | context.package.filename = npmPack(); 63 | context.package.path = path.join(process.cwd(), context.package.filename); 64 | 65 | context.workdir = setupTempWorkingDir(); 66 | 67 | npmInstall(context.package.path, context.workdir); 68 | 69 | context.testScripts.validate = copyTestScript( 70 | 'test-validate', 71 | context.workdir 72 | ); 73 | context.testScripts.export = copyTestScript('test-export', context.workdir); 74 | }); 75 | 76 | after(() => { 77 | fs.unlinkSync(context.package.path); 78 | fs.removeSync(context.workdir); 79 | }); 80 | 81 | it('package size should not change much', async () => { 82 | const baseUrl = 'https://registry.npmjs.org/zapier-platform-schema'; 83 | let res = await fetch(baseUrl); 84 | const packageInfo = await res.json(); 85 | const latestVersion = packageInfo['dist-tags'].latest; 86 | 87 | res = await fetch( 88 | `${baseUrl}/-/zapier-platform-schema-${latestVersion}.tgz`, 89 | { 90 | method: 'HEAD' 91 | } 92 | ); 93 | const baselineSize = res.headers.get('content-length'); 94 | const newSize = fs.statSync(context.package.path).size; 95 | newSize.should.be.within(baselineSize * 0.7, baselineSize * 1.3); 96 | }); 97 | 98 | it('should to able to validate app definitions', () => { 99 | const proc = spawnSync(context.testScripts.validate, { 100 | encoding: 'utf8', 101 | cwd: context.workdir 102 | }); 103 | const results = JSON.parse(proc.stdout); 104 | results.length.should.eql(2); 105 | results[0].length.should.eql(0); 106 | results[1].length.should.eql(2); 107 | results[1][0].should.eql('requires property "version"'); 108 | results[1][1].should.eql('requires property "platformVersion"'); 109 | }); 110 | 111 | it('exported-schema.json should exist and be up-to-date', () => { 112 | const exportedSchemaPath = path.join( 113 | context.workdir, 114 | 'node_modules', 115 | 'zapier-platform-schema', 116 | 'exported-schema.json' 117 | ); 118 | fs.existsSync(exportedSchemaPath).should.be.true(); 119 | 120 | const content = fs.readFileSync(exportedSchemaPath, { encoding: 'utf8' }); 121 | const schemaInPackage = JSON.parse(content); 122 | 123 | const proc = spawnSync(context.testScripts.export, { 124 | encoding: 'utf8', 125 | cwd: context.workdir 126 | }); 127 | const expectedSchema = JSON.parse(proc.stdout); 128 | 129 | schemaInPackage.should.eql( 130 | expectedSchema, 131 | 'exported-schema.json is not up-to-date. Try `npm run export`.' 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /lib/schemas/AppSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const AuthenticationSchema = require('./AuthenticationSchema'); 6 | const FlatObjectSchema = require('./FlatObjectSchema'); 7 | const ResourcesSchema = require('./ResourcesSchema'); 8 | const TriggersSchema = require('./TriggersSchema'); 9 | const SearchesSchema = require('./SearchesSchema'); 10 | const CreatesSchema = require('./CreatesSchema'); 11 | const SearchOrCreatesSchema = require('./SearchOrCreatesSchema'); 12 | const RequestSchema = require('./RequestSchema'); 13 | const VersionSchema = require('./VersionSchema'); 14 | const MiddlewaresSchema = require('./MiddlewaresSchema'); 15 | const HydratorsSchema = require('./HydratorsSchema'); 16 | const AppFlagsSchema = require('./AppFlagsSchema'); 17 | 18 | module.exports = makeSchema( 19 | { 20 | id: '/AppSchema', 21 | description: 'Represents a full app.', 22 | type: 'object', 23 | required: ['version', 'platformVersion'], 24 | properties: { 25 | version: { 26 | description: 'A version identifier for your code.', 27 | $ref: VersionSchema.id 28 | }, 29 | platformVersion: { 30 | description: 31 | 'A version identifier for the Zapier execution environment.', 32 | $ref: VersionSchema.id 33 | }, 34 | beforeApp: { 35 | description: 36 | 'EXPERIMENTAL: Before the perform method is called on your app, you can modify the execution context.', 37 | $ref: MiddlewaresSchema.id 38 | }, 39 | afterApp: { 40 | description: 41 | 'EXPERIMENTAL: After the perform method is called on your app, you can modify the response.', 42 | $ref: MiddlewaresSchema.id 43 | }, 44 | authentication: { 45 | description: 'Choose what scheme your API uses for authentication.', 46 | $ref: AuthenticationSchema.id 47 | }, 48 | requestTemplate: { 49 | description: 50 | 'Define a request mixin, great for setting custom headers, content-types, etc.', 51 | $ref: RequestSchema.id 52 | }, 53 | beforeRequest: { 54 | description: 55 | 'Before an HTTP request is sent via our `z.request()` client, you can modify it.', 56 | $ref: MiddlewaresSchema.id 57 | }, 58 | afterResponse: { 59 | description: 60 | 'After an HTTP response is recieved via our `z.request()` client, you can modify it.', 61 | $ref: MiddlewaresSchema.id 62 | }, 63 | hydrators: { 64 | description: 65 | "An optional bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.", 66 | $ref: HydratorsSchema.id 67 | }, 68 | resources: { 69 | description: 70 | 'All the resources for your app. Zapier will take these and generate the relevent triggers/searches/creates automatically.', 71 | $ref: ResourcesSchema.id 72 | }, 73 | triggers: { 74 | description: 75 | '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.', 76 | $ref: TriggersSchema.id 77 | }, 78 | searches: { 79 | description: 80 | '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.', 81 | $ref: SearchesSchema.id 82 | }, 83 | creates: { 84 | description: 85 | '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.', 86 | $ref: CreatesSchema.id 87 | }, 88 | searchOrCreates: { 89 | description: 90 | '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.', 91 | $ref: SearchOrCreatesSchema.id 92 | }, 93 | flags: { 94 | description: 'Top-level app options', 95 | $ref: AppFlagsSchema.id 96 | }, 97 | legacy: { 98 | description: 99 | '**INTERNAL USE ONLY**. Zapier uses this to hold properties from a legacy Web Builder app.', 100 | type: 'object', 101 | docAnnotation: { 102 | hide: true 103 | } 104 | } 105 | }, 106 | additionalProperties: false 107 | }, 108 | [ 109 | AuthenticationSchema, 110 | FlatObjectSchema, 111 | ResourcesSchema, 112 | TriggersSchema, 113 | SearchesSchema, 114 | CreatesSchema, 115 | SearchOrCreatesSchema, 116 | RequestSchema, 117 | VersionSchema, 118 | MiddlewaresSchema, 119 | HydratorsSchema, 120 | AppFlagsSchema 121 | ] 122 | ); 123 | -------------------------------------------------------------------------------- /test/readability.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('should'); 4 | 5 | const AuthenticationSchema = require('../lib/schemas/AuthenticationSchema'); 6 | const CreateSchema = require('../lib/schemas/CreateSchema'); 7 | const TriggerSchema = require('../lib/schemas/TriggerSchema'); 8 | 9 | describe('readability', () => { 10 | it('should have decent messages for anyOf mismatches', () => { 11 | const results = AuthenticationSchema.validate({ 12 | type: 'oauth2', 13 | test: 'whateverfake!' 14 | }); 15 | results.errors.should.have.length(1); 16 | results.errors[0].stack.should.eql('instance is not of a type(s) object'); 17 | should(results.errors[0].property.endsWith('instance')).be.false(); 18 | }); 19 | 20 | it('should have decent messages for minimum length not met', () => { 21 | const results = TriggerSchema.validate({ 22 | key: 'recipe', 23 | noun: 'Recipe', 24 | display: { 25 | label: '', 26 | description: 'Creates a new recipe.' 27 | }, 28 | operation: { 29 | perform: '$func$2$f$', 30 | sample: { id: 1 } 31 | } 32 | }); 33 | results.errors.should.have.length(1); 34 | should(results.errors[0].property.endsWith('instance')).be.false(); 35 | results.errors[0].stack.should.eql( 36 | 'instance.display.label does not meet minimum length of 2' 37 | ); 38 | }); 39 | 40 | it('should have decent messages for value type mismatch', () => { 41 | const results = CreateSchema.validate({ 42 | key: 'recipe', 43 | noun: 'Recipe', 44 | display: { 45 | label: 'Create Recipe', 46 | description: 'Creates a new recipe.' 47 | }, 48 | operation: { 49 | perform: '$func$2$f$', 50 | sample: { id: 1 }, 51 | inputFields: [123] 52 | } 53 | }); 54 | results.errors.should.have.length(1); 55 | should(results.errors[0].property.endsWith('instance')).be.false(); 56 | results.errors[0].stack.should.eql('instance is not of a type(s) object'); 57 | }); 58 | 59 | it('should handle falsy values for objects', () => { 60 | const results = CreateSchema.validate({ 61 | key: 'recipe', 62 | noun: 'Recipe', 63 | display: { 64 | label: 'Create Recipe', 65 | description: 'Creates a new recipe.' 66 | }, 67 | operation: { 68 | perform: '$func$2$f$', 69 | sample: { id: 1 }, 70 | inputFields: [0] 71 | } 72 | }); 73 | results.errors.should.have.length(1); 74 | should(results.errors[0].property.endsWith('instance')).be.false(); 75 | results.errors[0].stack.should.eql('instance is not of a type(s) object'); 76 | }); 77 | 78 | it('should surface deep issues', () => { 79 | const results = CreateSchema.validate({ 80 | key: 'recipe', 81 | noun: 'Recipe', 82 | display: { 83 | label: 'Create Recipe', 84 | description: 'Creates a new recipe.' 85 | }, 86 | operation: { 87 | perform: '$func$2$f$', 88 | sample: { id: 1 }, 89 | inputFields: [{ key: 'field', type: 'string', default: '' }] 90 | } 91 | }); 92 | results.errors.should.have.length(1); 93 | should(results.errors[0].property.endsWith('instance')).be.false(); 94 | results.errors[0].property.should.eql( 95 | 'instance.operation.inputFields[0].default' 96 | ); 97 | results.errors[0].message.should.eql('does not meet minimum length of 1'); 98 | }); 99 | 100 | it('should correctly surface subschema types', () => { 101 | const results = CreateSchema.validate({ 102 | key: 'recipe', 103 | noun: 'Recipe', 104 | display: { 105 | label: 'Create Recipe', 106 | description: 'Creates a new recipe.' 107 | }, 108 | operation: { 109 | perform: { 110 | url: 'https://example.com', 111 | body: 123 112 | }, 113 | sample: { id: 1 } 114 | } 115 | }); 116 | results.errors.should.have.length(1); 117 | results.errors[0].property.should.eql('instance.operation.perform.body'); 118 | should( 119 | results.errors[0].message.includes('null,string,object,array') 120 | ).be.true(); 121 | should(results.errors[0].property.endsWith('instance')).be.false(); 122 | results.errors[0].docLinks.length.should.eql(0); 123 | }); 124 | 125 | it('should be helpful for fieldChoices', () => { 126 | const results = CreateSchema.validate({ 127 | key: 'recipe', 128 | noun: 'Recipe', 129 | display: { 130 | label: 'Create Recipe', 131 | description: 'Creates a new recipe.' 132 | }, 133 | operation: { 134 | perform: '$func$2$f$', 135 | sample: { id: 1 }, 136 | inputFields: [ 137 | { 138 | key: 'adsf', 139 | // schema says these should be strings 140 | choices: [1, 2, 3] 141 | } 142 | ] 143 | } 144 | }); 145 | results.errors.should.have.length(1); 146 | results.errors[0].property.should.eql( 147 | 'instance.operation.inputFields[0].choices' 148 | ); 149 | should( 150 | results.errors[0].docLinks[0].endsWith('schema.md#fieldchoicesschema') 151 | ).be.true(); 152 | should(results.errors[0].property.endsWith('instance')).be.false(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /lib/schemas/FieldSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const RefResourceSchema = require('./RefResourceSchema'); 6 | 7 | const FieldChoicesSchema = require('./FieldChoicesSchema'); 8 | 9 | const { SKIP_KEY, INCOMPATIBLE_FIELD_SCHEMA_KEYS } = require('../constants'); 10 | 11 | // the following takes an array of string arrays (string[][]) and returns the follwing string: 12 | // * `a` & `b` 13 | // * `c` & `d` 14 | // ... etc 15 | const wrapInBackticks = s => `\`${s}\``; 16 | const formatBullet = f => `* ${f.map(wrapInBackticks).join(' & ')}`; 17 | const incompatibleFieldsList = INCOMPATIBLE_FIELD_SCHEMA_KEYS.map( 18 | formatBullet 19 | ).join('\n'); 20 | 21 | module.exports = makeSchema( 22 | { 23 | id: '/FieldSchema', 24 | description: `Defines a field an app either needs as input, or gives as output. In addition to the requirements below, the following keys are mutually exclusive:\n\n${incompatibleFieldsList}`, 25 | type: 'object', 26 | examples: [ 27 | { key: 'abc' }, 28 | { key: 'abc', choices: { mobile: 'Mobile Phone' } }, 29 | { key: 'abc', choices: ['first', 'second', 'third'] }, 30 | { 31 | key: 'abc', 32 | choices: [{ label: 'Red', sample: '#f00', value: '#f00' }] 33 | }, 34 | { key: 'abc', children: [{ key: 'abc' }] }, 35 | { key: 'abc', type: 'integer', helpText: 'neat' } 36 | ], 37 | antiExamples: [ 38 | {}, 39 | { key: 'abc', choices: {} }, 40 | { key: 'abc', choices: [] }, 41 | { key: 'abc', choices: [3] }, 42 | { key: 'abc', choices: [{ label: 'Red', value: '#f00' }] }, 43 | { key: 'abc', choices: 'mobile' }, 44 | { key: 'abc', type: 'loltype' }, 45 | { key: 'abc', children: [], helpText: '' }, 46 | { 47 | key: 'abc', 48 | children: [{ key: 'def', children: [] }] 49 | }, 50 | { 51 | key: 'abc', 52 | children: [{ key: 'def', children: [{ key: 'dhi' }] }], 53 | [SKIP_KEY]: true 54 | }, 55 | { key: 'abc', children: ['$func$2$f$'] } 56 | ], 57 | required: ['key'], 58 | properties: { 59 | key: { 60 | description: 61 | 'A unique machine readable key for this value (IE: "fname").', 62 | type: 'string', 63 | minLength: 1 64 | }, 65 | label: { 66 | description: 67 | 'A human readable label for this value (IE: "First Name").', 68 | type: 'string', 69 | minLength: 1 70 | }, 71 | helpText: { 72 | description: 73 | 'A human readable description of this value (IE: "The first part of a full name."). You can use Markdown.', 74 | type: 'string', 75 | minLength: 1, 76 | maxLength: 1000 77 | }, 78 | type: { 79 | description: 'The type of this value.', 80 | type: 'string', 81 | // string == unicode 82 | // text == a long textarea string 83 | // integer == int 84 | // number == float 85 | enum: [ 86 | 'string', 87 | 'text', 88 | 'integer', 89 | 'number', 90 | 'boolean', 91 | 'datetime', 92 | 'file', 93 | 'password', 94 | 'copy' 95 | ] 96 | }, 97 | required: { 98 | description: 'If this value is required or not.', 99 | type: 'boolean' 100 | }, 101 | placeholder: { 102 | description: 'An example value that is not saved.', 103 | type: 'string', 104 | minLength: 1 105 | }, 106 | default: { 107 | description: 108 | 'A default value that is saved the first time a Zap is created.', 109 | type: 'string', 110 | minLength: 1 111 | }, 112 | dynamic: { 113 | description: 114 | 'A reference to a trigger that will power a dynamic dropdown.', 115 | $ref: RefResourceSchema.id 116 | }, 117 | search: { 118 | description: 119 | 'A reference to a search that will guide the user to add a search step to populate this field when creating a Zap.', 120 | $ref: RefResourceSchema.id 121 | }, 122 | choices: { 123 | description: 124 | 'An object of machine keys and human values to populate a static dropdown.', 125 | $ref: FieldChoicesSchema.id 126 | }, 127 | list: { 128 | description: 'Can a user provide multiples of this field?', 129 | type: 'boolean' 130 | }, 131 | children: { 132 | type: 'array', 133 | items: { $ref: '/FieldSchema' }, 134 | description: 135 | 'An array of child fields that define the structure of a sub-object for this field. Usually used for line items.', 136 | minItems: 1 137 | }, 138 | dict: { 139 | description: 'Is this field a key/value input?', 140 | type: 'boolean' 141 | }, 142 | computed: { 143 | description: 144 | 'Is this field automatically populated (and hidden from the user)?', 145 | type: 'boolean' 146 | }, 147 | altersDynamicFields: { 148 | description: 149 | 'Does the value of this field affect the definitions of other fields in the set?', 150 | type: 'boolean' 151 | }, 152 | inputFormat: { 153 | description: 154 | '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").', 155 | type: 'string', 156 | // TODO: Check if it contains one and ONLY ONE '{{input}}' 157 | pattern: '^.*{{input}}.*$' 158 | } 159 | }, 160 | additionalProperties: false 161 | }, 162 | [RefResourceSchema, FieldChoicesSchema] 163 | ); 164 | -------------------------------------------------------------------------------- /lib/utils/makeValidator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsonschema = require('jsonschema'); 4 | const links = require('./links'); 5 | const functionalConstraints = require('../functional-constraints'); 6 | const { flattenDeep, get } = require('lodash'); 7 | 8 | const ambiguousTypes = ['anyOf', 'oneOf', 'allOf']; 9 | 10 | const makeLinks = (error, makerFunc) => { 11 | if (typeof error.schema === 'string') { 12 | return [makerFunc(error.schema)]; 13 | } 14 | if ( 15 | ambiguousTypes.includes(error.name) && 16 | error.argument && 17 | error.argument.length 18 | ) { 19 | // no way to know what the subschema was, so don't create links for it 20 | return error.argument 21 | .map(s => (s.includes('subschema') ? '' : makerFunc(s))) 22 | .filter(Boolean); 23 | } 24 | return []; 25 | }; 26 | 27 | const removeFirstAndLastChar = s => s.slice(1, -1); 28 | // always return a string 29 | const makePath = (path, newSegment) => 30 | (path ? [path, newSegment].join('.') : newSegment) || ''; 31 | 32 | const processBaseError = (err, path) => { 33 | const completePath = makePath(path, err.property) 34 | .replace(/\.instance\.?/g, '.') 35 | .replace(/\.instance$/, ''); 36 | 37 | const subSchemas = err.message.match(/\[subschema \d+\]/g); 38 | if (subSchemas) { 39 | subSchemas.forEach((subschema, idx) => { 40 | // err.schema is either an anonymous schema object or the name of a named schema 41 | if (typeof err.schema === 'string') { 42 | // this is basically only for FieldChoicesSchema and I'm not sure why 43 | err.message += ' Consult the docs below for valid subschemas.'; 44 | } else { 45 | // the subschemas have a type property 46 | err.message = err.message.replace( 47 | subschema, 48 | err.schema[err.name][idx].type || 'unknown' 49 | ); 50 | } 51 | }); 52 | } 53 | 54 | err.property = completePath; 55 | return err; 56 | }; 57 | 58 | /** 59 | * 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. 60 | * @param {ValidationError} validationError an individual error 61 | * @param {string} path current path in the error chain 62 | * @param {Validator} validator validator object to pass around that has all the schemas 63 | * @param {object} definition the original schema we're defining 64 | */ 65 | const cleanError = (validationError, path, validator, definition) => { 66 | if (ambiguousTypes.includes(validationError.name)) { 67 | // 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" 68 | if (validationError.argument.every(s => s.includes('subschema'))) { 69 | return processBaseError(validationError, path); 70 | } 71 | 72 | // Try against each of A, B, and C to take a guess as to which it's closed to 73 | // errorGroups will be an array of arrays of errors 74 | const errorGroups = validationError.argument.map((schemaName, idx) => { 75 | // this is what we'll validate against next 76 | let nextSchema; 77 | // schemaName is either "[subschema n]" or "/NamedSchema" 78 | if (schemaName.startsWith('[subschema')) { 79 | const maybeNamedSchema = validator.schemas[validationError.schema]; 80 | 81 | if (maybeNamedSchema) { 82 | nextSchema = maybeNamedSchema[validationError.name][idx]; 83 | } else { 84 | // hoist the anonymous subschema up 85 | nextSchema = validationError.schema[validationError.name][idx]; 86 | } 87 | } else { 88 | nextSchema = validator.schemas[removeFirstAndLastChar(schemaName)]; 89 | } 90 | 91 | if (validationError.instance === undefined) { 92 | // Work around a jsonschema bug: When the value being validated is 93 | // falsy, validationError.instance isn't available 94 | // See https://github.com/tdegrunt/jsonschema/issues/263 95 | const fullPath = 96 | path.replace(/^instance\./, '') + 97 | validationError.property.replace(/^instance\./, ''); 98 | validationError.instance = get(definition, fullPath); 99 | } 100 | 101 | const res = validator.validate(validationError.instance, nextSchema); 102 | 103 | return res.errors.map(e => 104 | cleanError( 105 | e, 106 | makePath(path, validationError.property), 107 | validator, 108 | definition 109 | ) 110 | ); 111 | }); 112 | 113 | // find the group with the fewest errors, that's probably the most accurate 114 | // if we're goign to tweak what gets returned, this is where we'll do it 115 | // a possible improvement could be treating a longer path favorably, like the python implementation does 116 | errorGroups.sort((a, b) => a.length - b.length); 117 | return errorGroups[0]; 118 | } else { 119 | // base case 120 | return processBaseError(validationError, path); 121 | } 122 | }; 123 | 124 | const makeValidator = (mainSchema, subSchemas) => { 125 | const schemas = [mainSchema].concat(subSchemas || []); 126 | const v = new jsonschema.Validator(); 127 | schemas.forEach(Schema => { 128 | v.addSchema(Schema, Schema.id); 129 | }); 130 | return { 131 | validate: definition => { 132 | const results = v.validate(definition, mainSchema); 133 | const allErrors = results.errors.concat( 134 | functionalConstraints.run(definition, mainSchema) 135 | ); 136 | const cleanedErrors = flattenDeep( 137 | allErrors.map(e => cleanError(e, '', v, definition)) 138 | ); 139 | 140 | results.errors = cleanedErrors.map(error => { 141 | error.codeLinks = makeLinks(error, links.makeCodeLink); 142 | error.docLinks = makeLinks(error, links.makeDocLink); 143 | return error; 144 | }); 145 | return results; 146 | } 147 | }; 148 | }; 149 | 150 | module.exports = makeValidator; 151 | -------------------------------------------------------------------------------- /test/functional-constraints/mutuallyExclusiveFields.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('should'); 4 | const schema = require('../../schema'); 5 | 6 | describe('mutuallyExclusiveFields', () => { 7 | it('should not error on fields not mutually exclusive', () => { 8 | const definition = { 9 | version: '1.0.0', 10 | platformVersion: '1.0.0', 11 | creates: { 12 | foo: { 13 | key: 'foo', 14 | noun: 'Foo', 15 | display: { 16 | label: 'Create Foo', 17 | description: 'Creates a...' 18 | }, 19 | operation: { 20 | perform: '$func$2$f$', 21 | sample: { id: 1 }, 22 | inputFields: [ 23 | { key: 'orderId', type: 'number' }, 24 | { 25 | key: 'line_items', 26 | children: [ 27 | { 28 | key: 'product', 29 | type: 'string' 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | }; 38 | 39 | const results = schema.validateAppDefinition(definition); 40 | results.errors.should.have.length(0); 41 | }); 42 | 43 | it('should error on fields that have children and list', () => { 44 | const definition = { 45 | version: '1.0.0', 46 | platformVersion: '1.0.0', 47 | creates: { 48 | foo: { 49 | key: 'foo', 50 | noun: 'Foo', 51 | display: { 52 | label: 'Create Foo', 53 | description: 'Creates a...' 54 | }, 55 | operation: { 56 | perform: '$func$2$f$', 57 | sample: { id: 1 }, 58 | inputFields: [ 59 | { key: 'orderId', type: 'number' }, 60 | { 61 | key: 'line_items', 62 | children: [ 63 | { 64 | key: 'product' 65 | } 66 | ], 67 | list: true 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | }; 74 | 75 | const results = schema.validateAppDefinition(definition); 76 | results.errors.should.have.length(1); 77 | results.errors[0].stack.should.eql( 78 | "instance.creates.foo.inputFields[1] must not contain children and list, as they're mutually exclusive." 79 | ); 80 | }); 81 | 82 | it('should error on fields that have list and dict', () => { 83 | const definition = { 84 | version: '1.0.0', 85 | platformVersion: '1.0.0', 86 | creates: { 87 | foo: { 88 | key: 'foo', 89 | noun: 'Foo', 90 | display: { 91 | label: 'Create Foo', 92 | description: 'Creates a...' 93 | }, 94 | operation: { 95 | perform: '$func$2$f$', 96 | sample: { id: 1 }, 97 | inputFields: [ 98 | { key: 'orderId', type: 'number' }, 99 | { 100 | key: 'line_items', 101 | dict: true, 102 | list: true 103 | } 104 | ] 105 | } 106 | } 107 | } 108 | }; 109 | 110 | const results = schema.validateAppDefinition(definition); 111 | results.errors.should.have.length(1); 112 | results.errors[0].stack.should.eql( 113 | "instance.creates.foo.inputFields[1] must not contain dict and list, as they're mutually exclusive." 114 | ); 115 | }); 116 | 117 | it('should error on fields that have dynamic and dict', () => { 118 | const definition = { 119 | version: '1.0.0', 120 | platformVersion: '1.0.0', 121 | creates: { 122 | foo: { 123 | key: 'foo', 124 | noun: 'Foo', 125 | display: { 126 | label: 'Create Foo', 127 | description: 'Creates a...' 128 | }, 129 | operation: { 130 | perform: '$func$2$f$', 131 | sample: { id: 1 }, 132 | inputFields: [ 133 | { 134 | key: 'orderId', 135 | type: 'number', 136 | dynamic: 'foo.id.number', 137 | dict: true 138 | }, 139 | { 140 | key: 'line_items', 141 | list: true 142 | } 143 | ] 144 | } 145 | } 146 | } 147 | }; 148 | 149 | const results = schema.validateAppDefinition(definition); 150 | results.errors.should.have.length(1); 151 | results.errors[0].stack.should.eql( 152 | "instance.creates.foo.inputFields[0] must not contain dynamic and dict, as they're mutually exclusive." 153 | ); 154 | }); 155 | 156 | it('should error on fields that have dynamic and choices', () => { 157 | const definition = { 158 | version: '1.0.0', 159 | platformVersion: '1.0.0', 160 | creates: { 161 | foo: { 162 | key: 'foo', 163 | noun: 'Foo', 164 | display: { 165 | label: 'Create Foo', 166 | description: 'Creates a...' 167 | }, 168 | operation: { 169 | perform: '$func$2$f$', 170 | sample: { id: 1 }, 171 | inputFields: [ 172 | { 173 | key: 'orderId', 174 | type: 'number', 175 | dynamic: 'foo.id.number', 176 | choices: { 177 | uno: 1, 178 | dos: 2 179 | } 180 | }, 181 | { 182 | key: 'line_items', 183 | list: true 184 | } 185 | ] 186 | } 187 | } 188 | } 189 | }; 190 | 191 | const results = schema.validateAppDefinition(definition); 192 | results.errors.should.have.length(1); 193 | results.errors[0].stack.should.eql( 194 | "instance.creates.foo.inputFields[0] must not contain dynamic and choices, as they're mutually exclusive." 195 | ); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /lib/utils/buildDocs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | 5 | const _ = require('lodash'); 6 | const toc = require('markdown-toc'); 7 | 8 | const packageJson = require('../../package.json'); 9 | const links = require('./links'); 10 | 11 | const NO_DESCRIPTION = '_No description given._'; 12 | const COMBOS = ['anyOf', 'allOf', 'oneOf']; 13 | const { SKIP_KEY } = require('../constants'); 14 | 15 | const walkSchemas = (InitSchema, callback) => { 16 | const recurse = (Schema, parents) => { 17 | parents = parents || []; 18 | callback(Schema, parents); 19 | Schema.dependencies.map(childSchema => { 20 | const newParents = parents.concat([InitSchema]); 21 | recurse(childSchema, newParents); 22 | }); 23 | }; 24 | recurse(InitSchema); 25 | }; 26 | 27 | const collectSchemas = InitSchema => { 28 | const schemas = {}; 29 | walkSchemas(InitSchema, Schema => { 30 | schemas[Schema.id] = Schema; 31 | }); 32 | return schemas; 33 | }; 34 | 35 | const BREAK_LENGTH = 96; 36 | const prepQuote = val => val.replace('`', ''); 37 | const quote = (val, triple, indent = '') => 38 | // either ``` with optional indentation or ` 39 | triple && val.length > BREAK_LENGTH 40 | ? '```\n' + 41 | val 42 | .match(/[^\r\n]+/g) 43 | .map(line => indent + line) 44 | .join('\n') + 45 | '\n' + 46 | indent + 47 | '```' 48 | : `\`${prepQuote(val)}\``; 49 | const quoteOrNa = (val, triple = false, indent = '') => 50 | val ? quote(val, triple, indent) : '_n/a_'; 51 | 52 | const formatExample = example => { 53 | const ex = _.isPlainObject(example) ? _.omit(example, SKIP_KEY) : example; 54 | return `* ${quoteOrNa( 55 | util.inspect(ex, { depth: null, breakLength: BREAK_LENGTH }), 56 | true, 57 | ' ' 58 | )}`; 59 | }; 60 | 61 | // Generate a display of the type (or link to a $ref). 62 | const typeOrLink = schema => { 63 | if (schema.type === 'array' && schema.items) { 64 | return `${quoteOrNa(schema.type)}[${typeOrLink(schema.items)}]`; 65 | } 66 | if (schema.$ref) { 67 | return `[${schema.$ref}](${links.anchor(schema.$ref)})`; 68 | } 69 | for (let i = 0; i < COMBOS.length; i++) { 70 | const key = COMBOS[i]; 71 | if (schema[key] && schema[key].length) { 72 | return `${key}(${schema[key].map(typeOrLink).join(', ')})`; 73 | } 74 | } 75 | if (schema.enum && schema.enum.length) { 76 | return `${quoteOrNa(schema.type)} in (${schema.enum 77 | .map(util.inspect) 78 | .map(quoteOrNa) 79 | .join(', ')})`; 80 | } 81 | return quoteOrNa(schema.type); 82 | }; 83 | 84 | // Properly quote and display examples. 85 | const makeExampleSection = Schema => { 86 | const examples = Schema.schema.examples || []; 87 | if (!examples.length) { 88 | return ''; 89 | } 90 | return `\ 91 | #### Examples 92 | 93 | ${examples.map(formatExample).join('\n')} 94 | `; 95 | }; 96 | 97 | // Properly quote and display anti-examples. 98 | const makeAntiExampleSection = Schema => { 99 | const examples = Schema.schema.antiExamples || []; 100 | if (!examples.length) { 101 | return ''; 102 | } 103 | return `\ 104 | #### Anti-Examples 105 | 106 | ${examples.map(formatExample).join('\n')} 107 | `; 108 | }; 109 | 110 | const processProperty = (key, property, propIsRequired) => { 111 | let isRequired = propIsRequired ? '**yes**' : 'no'; 112 | if (_.get(property, 'docAnnotation.hide')) { 113 | return ''; 114 | } else if (_.get(property, 'docAnnotation.required')) { 115 | // can also support keys besides "required" 116 | const annotation = property.docAnnotation.required; 117 | if (annotation.type === 'replace') { 118 | isRequired = annotation.value; 119 | } else if (annotation.type === 'append') { 120 | isRequired += annotation.value; 121 | } else { 122 | throw new Error(`unrecognized docAnnotation type: ${annotation.type}`); 123 | } 124 | } 125 | return `${quoteOrNa(key)} | ${isRequired} | ${typeOrLink( 126 | property 127 | )} | ${property.description || NO_DESCRIPTION}`; 128 | }; 129 | 130 | // Enumerate the properties as a table. 131 | const makePropertiesSection = Schema => { 132 | const properties = 133 | Schema.schema.properties || Schema.schema.patternProperties || {}; 134 | if (!Object.keys(properties).length) { 135 | return ''; 136 | } 137 | const required = Schema.schema.required || []; 138 | return `\ 139 | #### Properties 140 | 141 | Key | Required | Type | Description 142 | --- | -------- | ---- | ----------- 143 | ${Object.keys(properties) 144 | .map(key => { 145 | const property = properties[key]; 146 | return processProperty(key, property, required.includes(key)); 147 | }) 148 | .join('\n')} 149 | `; 150 | }; 151 | 152 | // Given a "root" schema, create some markdown. 153 | const makeMarkdownSection = Schema => { 154 | return `\ 155 | ## ${Schema.id} 156 | 157 | ${Schema.schema.description || NO_DESCRIPTION} 158 | 159 | #### Details 160 | 161 | * **Type** - ${typeOrLink(Schema.schema)} 162 | * **Pattern** - ${quoteOrNa(Schema.schema.pattern)} 163 | * **Source Code** - [lib/schemas${Schema.id}.js](${links.makeCodeLink( 164 | Schema.id 165 | )}) 166 | 167 | ${makeExampleSection(Schema)} 168 | ${makeAntiExampleSection(Schema)} 169 | ${makePropertiesSection(Schema)} 170 | `.trim(); 171 | }; 172 | 173 | // Generate the final markdown. 174 | const buildDocs = InitSchema => { 175 | const schemas = collectSchemas(InitSchema); 176 | const markdownSections = _.chain(schemas) 177 | .values() 178 | .sortBy('id') 179 | .map(makeMarkdownSection) 180 | .join('\n\n-----\n\n'); 181 | const docs = `\ 182 | 183 | # \`zapier-platform-schema\` Generated Documentation 184 | 185 | This is automatically generated by the \`npm run docs\` command in \`zapier-platform-schema\` version ${quoteOrNa( 186 | packageJson.version 187 | )}. 188 | 189 | ----- 190 | 191 | ## Index 192 | 193 | 194 | ----- 195 | 196 | ${markdownSections} 197 | 198 | `.trim(); 199 | return toc.insert(docs, { maxdepth: 2, bullets: '*' }); 200 | }; 201 | 202 | module.exports = buildDocs; 203 | -------------------------------------------------------------------------------- /lib/schemas/ResourceSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const makeSchema = require('../utils/makeSchema'); 4 | 5 | const ResourceMethodGetSchema = require('./ResourceMethodGetSchema'); 6 | const ResourceMethodHookSchema = require('./ResourceMethodHookSchema'); 7 | const ResourceMethodListSchema = require('./ResourceMethodListSchema'); 8 | const ResourceMethodSearchSchema = require('./ResourceMethodSearchSchema'); 9 | const ResourceMethodCreateSchema = require('./ResourceMethodCreateSchema'); 10 | const DynamicFieldsSchema = require('./DynamicFieldsSchema'); 11 | const KeySchema = require('./KeySchema'); 12 | 13 | module.exports = makeSchema( 14 | { 15 | id: '/ResourceSchema', 16 | description: 17 | 'Represents a resource, which will in turn power triggers, searches, or creates.', 18 | type: 'object', 19 | required: ['key', 'noun'], 20 | examples: [ 21 | { 22 | key: 'tag', 23 | noun: 'Tag', 24 | get: { 25 | display: { 26 | label: 'Get Tag by ID', 27 | description: 'Grab a specific Tag by ID.' 28 | }, 29 | operation: { 30 | perform: { 31 | url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' 32 | }, 33 | sample: { 34 | id: 385, 35 | name: 'proactive enable ROI' 36 | } 37 | } 38 | } 39 | }, 40 | { 41 | key: 'tag', 42 | noun: 'Tag', 43 | sample: { 44 | id: 385, 45 | name: 'proactive enable ROI' 46 | }, 47 | get: { 48 | display: { 49 | label: 'Get Tag by ID', 50 | description: 'Grab a specific Tag by ID.' 51 | }, 52 | operation: { 53 | perform: { 54 | url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' 55 | } 56 | // resource sample is used 57 | } 58 | } 59 | }, 60 | { 61 | key: 'tag', 62 | noun: 'Tag', 63 | get: { 64 | display: { 65 | label: 'Get Tag by ID', 66 | description: 'Grab a specific Tag by ID.', 67 | hidden: true 68 | }, 69 | operation: { 70 | perform: { 71 | url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' 72 | } 73 | } 74 | }, 75 | list: { 76 | display: { 77 | label: 'New Tag', 78 | description: 'Trigger when a new Tag is created in your account.' 79 | }, 80 | operation: { 81 | perform: { 82 | url: 'http://fake-crm.getsandbox.com/tags' 83 | }, 84 | sample: { 85 | id: 385, 86 | name: 'proactive enable ROI' 87 | } 88 | } 89 | } 90 | } 91 | ], 92 | antiExamples: [ 93 | { 94 | key: 'tag', 95 | noun: 'Tag', 96 | get: { 97 | display: { 98 | label: 'Get Tag by ID', 99 | description: 'Grab a specific Tag by ID.' 100 | }, 101 | operation: { 102 | perform: { 103 | url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' 104 | } 105 | // missing sample (and no sample on resource) 106 | } 107 | }, 108 | list: { 109 | display: { 110 | label: 'New Tag', 111 | description: 'Trigger when a new Tag is created in your account.' 112 | }, 113 | operation: { 114 | perform: { 115 | url: 'http://fake-crm.getsandbox.com/tags' 116 | }, 117 | sample: { 118 | id: 385, 119 | name: 'proactive enable ROI' 120 | } 121 | } 122 | } 123 | }, 124 | { 125 | key: 'tag', 126 | noun: 'Tag', 127 | get: { 128 | display: { 129 | label: 'Get Tag by ID', 130 | description: 'Grab a specific Tag by ID.' 131 | }, 132 | operation: { 133 | perform: { 134 | url: 'http://fake-crm.getsandbox.com/tags/{{inputData.id}}' 135 | } 136 | // missing sample (and no sample on resource) 137 | } 138 | } 139 | } 140 | ], 141 | properties: { 142 | key: { 143 | description: 'A key to uniquely identify this resource.', 144 | $ref: KeySchema.id 145 | }, 146 | noun: { 147 | description: 148 | 'A noun for this resource that completes the sentence "create a new XXX".', 149 | type: 'string', 150 | minLength: 2, 151 | maxLength: 255 152 | }, 153 | // TODO: do we need to break these all apart too? :-/ 154 | get: { 155 | description: ResourceMethodGetSchema.schema.description, 156 | $ref: ResourceMethodGetSchema.id 157 | }, 158 | hook: { 159 | description: ResourceMethodHookSchema.schema.description, 160 | $ref: ResourceMethodHookSchema.id 161 | }, 162 | list: { 163 | description: ResourceMethodListSchema.schema.description, 164 | $ref: ResourceMethodListSchema.id 165 | }, 166 | search: { 167 | description: ResourceMethodSearchSchema.schema.description, 168 | $ref: ResourceMethodSearchSchema.id 169 | }, 170 | create: { 171 | description: ResourceMethodCreateSchema.schema.description, 172 | $ref: ResourceMethodCreateSchema.id 173 | }, 174 | outputFields: { 175 | description: 'What fields of data will this return?', 176 | $ref: DynamicFieldsSchema.id 177 | }, 178 | sample: { 179 | description: 'What does a sample of data look like?', 180 | type: 'object', 181 | // TODO: require id, ID, Id property? 182 | minProperties: 1 183 | } 184 | }, 185 | additionalProperties: false 186 | }, 187 | [ 188 | ResourceMethodGetSchema, 189 | ResourceMethodHookSchema, 190 | ResourceMethodListSchema, 191 | ResourceMethodSearchSchema, 192 | ResourceMethodCreateSchema, 193 | DynamicFieldsSchema, 194 | KeySchema 195 | ] 196 | ); 197 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('should'); 4 | const schema = require('../schema'); 5 | 6 | const testUtils = require('./utils'); 7 | 8 | const appDefinition = require('../examples/definition.json'); 9 | 10 | const copy = o => JSON.parse(JSON.stringify(o)); 11 | 12 | const NUM_SCHEMAS = 48; // changes regularly as we expand 13 | 14 | describe('app', () => { 15 | describe('validation', () => { 16 | it('should be a valid app', () => { 17 | const results = schema.validateAppDefinition(appDefinition); 18 | results.errors.should.eql([]); 19 | }); 20 | 21 | it('should invalidate deep errors', () => { 22 | const appCopy = copy(appDefinition); 23 | delete appCopy.version; 24 | delete appCopy.triggers.contact_by_tag.noun; 25 | delete appCopy.triggers.contact_by_tag.display.label; 26 | appCopy.triggers.contact_by_tag.operation.inputFields[0].type = 'loltype'; 27 | const results = schema.validateAppDefinition(appCopy); 28 | results.errors.length.should.eql(4); 29 | }); 30 | 31 | it('should invalidate a bad named trigger', () => { 32 | const appCopy = copy(appDefinition); 33 | appCopy.triggers['3contact_by_tag'] = appCopy.triggers.contact_by_tag; 34 | delete appCopy.triggers.contact_by_tag; 35 | const results = schema.validateAppDefinition(appCopy); 36 | results.errors.length.should.eql(2); // invalid name and top-level key doesn't match trigger key 37 | }); 38 | 39 | it('should run and pass functional constraints', function() { 40 | const definition = { 41 | version: '1.0.0', 42 | platformVersion: '1.0.0', 43 | searches: { 44 | fooSearch: { 45 | key: 'fooSearch', 46 | noun: 'Foo', 47 | display: { 48 | label: 'Find Foo', 49 | description: 'Find a foo...' 50 | }, 51 | operation: { 52 | perform: '$func$2$f$', 53 | sample: { id: 1 } 54 | } 55 | } 56 | }, 57 | creates: { 58 | fooCreate: { 59 | key: 'fooCreate', 60 | noun: 'Foo', 61 | display: { 62 | label: 'Create Foo', 63 | description: 'Creates a...' 64 | }, 65 | operation: { 66 | perform: '$func$2$f$', 67 | sample: { id: 1 } 68 | } 69 | } 70 | }, 71 | searchOrCreates: { 72 | fooSearchOrCreate: { 73 | key: 'fooSearch', 74 | display: { 75 | label: 'Find or Create a...', 76 | description: 'Something Something' 77 | }, 78 | search: 'fooSearch', 79 | create: 'fooCreate' 80 | } 81 | } 82 | }; 83 | const results = schema.validateAppDefinition(definition); 84 | results.errors.should.have.length(0); 85 | }); 86 | 87 | it('should run functional constraints with errors', function() { 88 | const definition = { 89 | version: '1.0.0', 90 | platformVersion: '1.0.0', 91 | searches: { 92 | fooSearch: { 93 | key: 'fooSearch', 94 | noun: 'Foo', 95 | display: { 96 | label: 'Find Foo', 97 | description: 'Find a foo...' 98 | }, 99 | operation: { 100 | perform: '$func$2$f$', 101 | sample: { id: 1 } 102 | } 103 | } 104 | }, 105 | creates: { 106 | fooCreate: { 107 | key: 'fooCreate', 108 | noun: 'Foo', 109 | display: { 110 | label: 'Create Foo', 111 | description: 'Creates a...' 112 | }, 113 | operation: { 114 | perform: '$func$2$f$', 115 | sample: { id: 1 } 116 | } 117 | } 118 | }, 119 | searchOrCreates: { 120 | fooSearchOrCreate: { 121 | key: 'fooSearchOrCreate', 122 | display: { 123 | label: 'Find or Create a...', 124 | description: 'Something Something' 125 | }, 126 | search: 'fooBad', 127 | create: 'fooBad' 128 | } 129 | } 130 | }; 131 | const results = schema.validateAppDefinition(definition); 132 | results.errors.should.have.length(3); 133 | results.errors[0].stack.should.eql( 134 | 'instance.searchOrCreates.fooSearchOrCreate.key must match a "key" from a search (options: fooSearch)' 135 | ); 136 | results.errors[1].stack.should.eql( 137 | 'instance.searchOrCreates.fooSearchOrCreate.search must match a "key" from a search (options: fooSearch)' 138 | ); 139 | results.errors[2].stack.should.eql( 140 | 'instance.searchOrCreates.fooSearchOrCreate.create must match a "key" from a create (options: fooCreate)' 141 | ); 142 | }); 143 | 144 | it('should validate inputFormat', () => { 145 | const appCopy = copy(appDefinition); 146 | appCopy.authentication = { 147 | type: 'custom', 148 | test: { 149 | url: 'https://example.com' 150 | }, 151 | fields: [ 152 | { 153 | key: 'subdomain', 154 | type: 'string', 155 | required: true, 156 | inputFormat: 'https://{{input}}.example.com' 157 | } 158 | ] 159 | }; 160 | const results = schema.validateAppDefinition(appCopy); 161 | // this line ensures we're getting a ValidatorResult class, not just an object that looks like one 162 | should(results.valid).eql(true); 163 | results.errors.should.eql([]); 164 | }); 165 | 166 | it('should invalidate illegal inputFormat', () => { 167 | const appCopy = copy(appDefinition); 168 | appCopy.authentication = { 169 | type: 'custom', 170 | test: { 171 | url: 'https://example.com' 172 | }, 173 | fields: [ 174 | { 175 | key: 'subdomain', 176 | type: 'string', 177 | required: true, 178 | inputFormat: 'https://{{input}.example.com' 179 | } 180 | ] 181 | }; 182 | const results = schema.validateAppDefinition(appCopy); 183 | results.errors.length.should.eql(1); 184 | should(results.valid).eql(false); 185 | 186 | const error = results.errors[0]; 187 | error.name.should.eql('pattern'); 188 | error.instance.should.eql('https://{{input}.example.com'); 189 | }); 190 | 191 | it('should validate legacy properties', () => { 192 | const appCopy = copy(appDefinition); 193 | appCopy.legacy = { 194 | subscribeUrl: 'https://example.com', 195 | triggers: { 196 | contact: { 197 | url: 'https://example.com' 198 | } 199 | } 200 | }; 201 | const results = schema.validateAppDefinition(appCopy); 202 | results.errors.should.eql([]); 203 | }); 204 | }); 205 | 206 | describe('export', () => { 207 | it('should export the full schema', () => { 208 | const exportedSchema = schema.exportSchema(); 209 | Object.keys(exportedSchema.schemas).length.should.eql(NUM_SCHEMAS); 210 | }); 211 | }); 212 | }); 213 | 214 | describe('auto test', () => { 215 | const _exportedSchema = schema.exportSchema(); 216 | Object.keys(_exportedSchema.schemas).map(id => { 217 | testUtils.testInlineSchemaExamples(id); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /examples/definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.30", 3 | "platformVersion": "1.2.30", 4 | "beforeRequest": "$func$2$f$", 5 | "afterResponse": ["$func$2$f$"], 6 | "requestTemplate": { 7 | "headers": { 8 | "X-MyCustomHeader": "my header value" 9 | } 10 | }, 11 | "authentication": { 12 | "type": "oauth2", 13 | "test": { 14 | "url": "http://fake-crm.getsandbox.com/ping", 15 | "method": "GET" 16 | }, 17 | "fields": [ 18 | { 19 | "key": "access_token", 20 | "computed": true 21 | }, 22 | { 23 | "key": "refresh_token", 24 | "computed": true 25 | } 26 | ], 27 | "oauth2Config": { 28 | "authorizeUrl": "$func$2$f$", 29 | "getAccessToken": { 30 | "url": "http://fake-crm.getsandbox.com/oauth/access_token", 31 | "method": "POST" 32 | }, 33 | "scope": "tags,users,contacts", 34 | "autoRefresh": false 35 | }, 36 | "connectionLabel": "{{inputData.email}}" 37 | }, 38 | "resources": { 39 | "tag": { 40 | "key": "tag", 41 | "noun": "Tag", 42 | "sample": { 43 | "id": 1, 44 | "name": "test tag" 45 | }, 46 | "get": { 47 | "display": { 48 | "label": "Get Tag by ID", 49 | "description": "Grab a specific Tag by ID." 50 | }, 51 | "operation": { 52 | "perform": { 53 | "url": "http://fake-crm.getsandbox.com/tags/{{inputData.id}}" 54 | }, 55 | "sample": { 56 | "id": 385, 57 | "name": "proactive enable ROI" 58 | }, 59 | "inputFields": [ 60 | { 61 | "key": "id", 62 | "required": true 63 | } 64 | ] 65 | } 66 | }, 67 | "list": { 68 | "display": { 69 | "label": "New Tag", 70 | "description": "Trigger when a new Tag is created in your account." 71 | }, 72 | "operation": { 73 | "perform": { 74 | "url": "http://fake-crm.getsandbox.com/tags" 75 | }, 76 | "sample": { 77 | "id": 385, 78 | "name": "proactive enable ROI" 79 | } 80 | } 81 | }, 82 | "create": { 83 | "display": { 84 | "label": "Create Tag", 85 | "description": "Create a new Tag in your account." 86 | }, 87 | "operation": { 88 | "perform": "$func$2$f$", 89 | "sample": { 90 | "id": 1, 91 | "name": "proactive enable ROI" 92 | } 93 | } 94 | } 95 | }, 96 | "user": { 97 | "key": "user", 98 | "noun": "User", 99 | "sample": { 100 | "id": 1, 101 | "name": "Test McTesterson", 102 | "email": "test@mctesterson.com" 103 | }, 104 | "get": { 105 | "display": { 106 | "label": "Get User by ID", 107 | "description": "Grab a specific User by ID." 108 | }, 109 | "operation": { 110 | "perform": { 111 | "url": "http://fake-crm.getsandbox.com/users/{{inputData.id}}" 112 | }, 113 | "sample": { 114 | "id": 1, 115 | "name": "Jalen Bode", 116 | "email": "jalen.bode@company.com" 117 | }, 118 | "inputFields": [ 119 | { 120 | "key": "id", 121 | "required": true 122 | } 123 | ] 124 | } 125 | }, 126 | "list": { 127 | "display": { 128 | "label": "New User", 129 | "description": "Trigger when a new User is created in your account." 130 | }, 131 | "operation": { 132 | "perform": { 133 | "url": "http://fake-crm.getsandbox.com/users" 134 | }, 135 | "sample": { 136 | "id": 49, 137 | "name": "Veronica Kuhn", 138 | "email": "veronica.kuhn@company.com" 139 | } 140 | } 141 | } 142 | }, 143 | "contact": { 144 | "key": "contact", 145 | "noun": "Contact", 146 | "sample": { 147 | "id": 1, 148 | "name": "Test Contact", 149 | "company": "Test Inc", 150 | "email": "test@example.com.com", 151 | "phone": "1-111-555-7000", 152 | "address": "1234 Test Canyon", 153 | "owner_id": 1, 154 | "tag_ids": [1, 2, 3] 155 | }, 156 | "get": { 157 | "display": { 158 | "label": "Get Contact by ID", 159 | "description": "Grab a specific Contact by ID." 160 | }, 161 | "operation": { 162 | "perform": { 163 | "url": "http://fake-crm.getsandbox.com/contacts/{{inputData.id}}" 164 | }, 165 | "sample": { 166 | "id": 1, 167 | "name": "Rosalee Kub", 168 | "company": "Schmidt, O'Reilly and Moen", 169 | "email": "Rosalee_Kub47@hotmail.com", 170 | "phone": "412-916-6798 x3478", 171 | "address": "73375 Jacobson Turnpike", 172 | "owner_id": 9, 173 | "tag_ids": [87] 174 | }, 175 | "inputFields": [ 176 | { 177 | "key": "id", 178 | "required": true 179 | } 180 | ] 181 | } 182 | }, 183 | "list": { 184 | "display": { 185 | "label": "New Contact", 186 | "description": 187 | "Trigger when a new Contact is created in your account." 188 | }, 189 | "operation": { 190 | "perform": { 191 | "url": "http://fake-crm.getsandbox.com/contacts" 192 | }, 193 | "sample": { 194 | "id": 1, 195 | "name": "Rosalee Kub", 196 | "company": "Schmidt, O'Reilly and Moen", 197 | "email": "Rosalee_Kub47@hotmail.com", 198 | "phone": "412-916-6798 x3478", 199 | "address": "73375 Jacobson Turnpike", 200 | "owner_id": 9, 201 | "tag_ids": [87] 202 | } 203 | } 204 | }, 205 | "create": { 206 | "display": { 207 | "label": "Create Contact", 208 | "description": "Create a new Contact in your account." 209 | }, 210 | "operation": { 211 | "perform": "$func$2$f$", 212 | "sample": { 213 | "id": 1, 214 | "name": "Rosalee Kub", 215 | "company": "Schmidt, O'Reilly and Moen", 216 | "email": "Rosalee_Kub47@hotmail.com", 217 | "phone": "412-916-6798 x3478", 218 | "address": "73375 Jacobson Turnpike", 219 | "owner_id": 9, 220 | "tag_ids": [87] 221 | }, 222 | "inputFields": [ 223 | { 224 | "key": "email", 225 | "label": "Email", 226 | "type": "string", 227 | "required": true 228 | }, 229 | { 230 | "key": "name", 231 | "label": "Name", 232 | "type": "string" 233 | }, 234 | { 235 | "key": "company", 236 | "label": "Company", 237 | "type": "string" 238 | }, 239 | { 240 | "key": "phone", 241 | "label": "Phone", 242 | "type": "string" 243 | }, 244 | { 245 | "key": "address", 246 | "label": "Address", 247 | "type": "text" 248 | }, 249 | { 250 | "key": "owner_id", 251 | "label": "Owner", 252 | "type": "integer", 253 | "dynamic": "user" 254 | }, 255 | { 256 | "key": "tag_id", 257 | "label": "Tag", 258 | "type": "integer", 259 | "dynamic": "tag.id.name", 260 | "search": "tag.id" 261 | } 262 | ] 263 | } 264 | } 265 | } 266 | }, 267 | "triggers": { 268 | "contact_by_tag": { 269 | "key": "contact_by_tag", 270 | "noun": "Contact", 271 | "display": { 272 | "label": "New Tagged Contact", 273 | "description": "Trigger when a new Contact is tagged in your account." 274 | }, 275 | "operation": { 276 | "resource": "contact", 277 | "perform": { 278 | "url": 279 | "http://fake-crm.getsandbox.com/contacts?tag_id={{inputData.tagId}}" 280 | }, 281 | "inputFields": [ 282 | { 283 | "key": "tagId", 284 | "label": "Tag", 285 | "type": "integer", 286 | "dynamic": "tag" 287 | } 288 | ], 289 | "sample": { 290 | "id": 1, 291 | "name": "Test Contact", 292 | "company": "Test Inc", 293 | "email": "test@example.com.com", 294 | "phone": "1-111-555-7000", 295 | "address": "1234 Test Canyon", 296 | "owner_id": 1, 297 | "tag_ids": [1, 2, 3] 298 | } 299 | } 300 | }, 301 | "tag_list": { 302 | "display": { 303 | "label": "New Tag", 304 | "description": "Trigger when a new Tag is created in your account." 305 | }, 306 | "operation": { 307 | "perform": { 308 | "url": "http://fake-crm.getsandbox.com/tags" 309 | }, 310 | "sample": { 311 | "id": 385, 312 | "name": "proactive enable ROI" 313 | }, 314 | "resource": "tag", 315 | "type": "polling" 316 | }, 317 | "key": "tag_list", 318 | "noun": "Tag" 319 | }, 320 | "user_list": { 321 | "display": { 322 | "label": "New User", 323 | "description": "Trigger when a new User is created in your account." 324 | }, 325 | "operation": { 326 | "perform": { 327 | "url": "http://fake-crm.getsandbox.com/users" 328 | }, 329 | "sample": { 330 | "id": 49, 331 | "name": "Veronica Kuhn", 332 | "email": "veronica.kuhn@company.com" 333 | }, 334 | "resource": "user", 335 | "type": "polling" 336 | }, 337 | "key": "user_list", 338 | "noun": "User" 339 | }, 340 | "contact_list": { 341 | "display": { 342 | "label": "New Contact", 343 | "description": "Trigger when a new Contact is created in your account." 344 | }, 345 | "operation": { 346 | "perform": { 347 | "url": "http://fake-crm.getsandbox.com/contacts" 348 | }, 349 | "resource": "contact", 350 | "type": "polling", 351 | "sample": { 352 | "id": 1, 353 | "name": "Test Contact", 354 | "company": "Test Inc", 355 | "email": "test@example.com.com", 356 | "phone": "1-111-555-7000", 357 | "address": "1234 Test Canyon", 358 | "owner_id": 1, 359 | "tag_ids": [1, 2, 3] 360 | } 361 | }, 362 | "key": "contact_list", 363 | "noun": "Contact" 364 | } 365 | }, 366 | "searches": { 367 | "tag": { 368 | "display": { 369 | "label": "Find Tag", 370 | "description": "Finds a Tag." 371 | }, 372 | "operation": { 373 | "perform": { 374 | "url": "http://fake-crm.getsandbox.com/tags?name={{inputData.name}}" 375 | }, 376 | "sample": { 377 | "id": 385, 378 | "name": "proactive enable ROI" 379 | }, 380 | "resource": "tag", 381 | "inputFields": [ 382 | { 383 | "key": "name", 384 | "label": "Tag Name", 385 | "type": "string" 386 | } 387 | ], 388 | "sample": { 389 | "id": 1, 390 | "name": "test tag" 391 | } 392 | }, 393 | "key": "tag", 394 | "noun": "Tag" 395 | } 396 | }, 397 | "creates": { 398 | "tag_create": { 399 | "display": { 400 | "label": "Create Tag", 401 | "description": "Create a new Tag in your account." 402 | }, 403 | "operation": { 404 | "perform": "$func$2$f$", 405 | "resource": "tag", 406 | "sample": { 407 | "id": 385, 408 | "name": "proactive enable ROI" 409 | } 410 | }, 411 | "key": "tag_create", 412 | "noun": "Tag" 413 | }, 414 | "contact_create": { 415 | "display": { 416 | "label": "Create Contact", 417 | "description": "Create a new Contact in your account." 418 | }, 419 | "operation": { 420 | "perform": "$func$2$f$", 421 | "inputFields": [ 422 | { 423 | "key": "email", 424 | "label": "Email", 425 | "type": "string", 426 | "required": true 427 | }, 428 | { 429 | "key": "name", 430 | "label": "Name", 431 | "type": "string" 432 | }, 433 | { 434 | "key": "company", 435 | "label": "Company", 436 | "type": "string" 437 | }, 438 | { 439 | "key": "phone", 440 | "label": "Phone", 441 | "type": "string" 442 | }, 443 | { 444 | "key": "address", 445 | "label": "Address", 446 | "type": "text" 447 | }, 448 | { 449 | "key": "owner_id", 450 | "label": "Owner", 451 | "type": "integer", 452 | "dynamic": "user" 453 | }, 454 | { 455 | "key": "tag_id", 456 | "label": "Tag", 457 | "type": "integer", 458 | "dynamic": "tag" 459 | } 460 | ], 461 | "resource": "contact", 462 | "sample": { 463 | "id": 1, 464 | "name": "Test Contact", 465 | "company": "Test Inc", 466 | "email": "test@example.com.com", 467 | "phone": "1-111-555-7000", 468 | "address": "1234 Test Canyon", 469 | "owner_id": 1, 470 | "tag_ids": [1, 2, 3] 471 | } 472 | }, 473 | "key": "contact_create", 474 | "noun": "Contact" 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /exported-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "8.2.1", 3 | "schemas": { 4 | "AppSchema": { 5 | "id": "/AppSchema", 6 | "description": "Represents a full app.", 7 | "type": "object", 8 | "required": ["version", "platformVersion"], 9 | "properties": { 10 | "version": { 11 | "description": "A version identifier for your code.", 12 | "$ref": "/VersionSchema" 13 | }, 14 | "platformVersion": { 15 | "description": 16 | "A version identifier for the Zapier execution environment.", 17 | "$ref": "/VersionSchema" 18 | }, 19 | "beforeApp": { 20 | "description": 21 | "EXPERIMENTAL: Before the perform method is called on your app, you can modify the execution context.", 22 | "$ref": "/MiddlewaresSchema" 23 | }, 24 | "afterApp": { 25 | "description": 26 | "EXPERIMENTAL: After the perform method is called on your app, you can modify the response.", 27 | "$ref": "/MiddlewaresSchema" 28 | }, 29 | "authentication": { 30 | "description": "Choose what scheme your API uses for authentication.", 31 | "$ref": "/AuthenticationSchema" 32 | }, 33 | "requestTemplate": { 34 | "description": 35 | "Define a request mixin, great for setting custom headers, content-types, etc.", 36 | "$ref": "/RequestSchema" 37 | }, 38 | "beforeRequest": { 39 | "description": 40 | "Before an HTTP request is sent via our `z.request()` client, you can modify it.", 41 | "$ref": "/MiddlewaresSchema" 42 | }, 43 | "afterResponse": { 44 | "description": 45 | "After an HTTP response is recieved via our `z.request()` client, you can modify it.", 46 | "$ref": "/MiddlewaresSchema" 47 | }, 48 | "hydrators": { 49 | "description": 50 | "An optional bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.", 51 | "$ref": "/HydratorsSchema" 52 | }, 53 | "resources": { 54 | "description": 55 | "All the resources for your app. Zapier will take these and generate the relevent triggers/searches/creates automatically.", 56 | "$ref": "/ResourcesSchema" 57 | }, 58 | "triggers": { 59 | "description": 60 | "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.", 61 | "$ref": "/TriggersSchema" 62 | }, 63 | "searches": { 64 | "description": 65 | "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.", 66 | "$ref": "/SearchesSchema" 67 | }, 68 | "creates": { 69 | "description": 70 | "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.", 71 | "$ref": "/CreatesSchema" 72 | }, 73 | "searchOrCreates": { 74 | "description": 75 | "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.", 76 | "$ref": "/SearchOrCreatesSchema" 77 | }, 78 | "flags": { 79 | "description": "Top-level app options", 80 | "$ref": "/AppFlagsSchema" 81 | }, 82 | "legacy": { 83 | "description": 84 | "**INTERNAL USE ONLY**. Zapier uses this to hold properties from a legacy Web Builder app.", 85 | "type": "object", 86 | "docAnnotation": { 87 | "hide": true 88 | } 89 | } 90 | }, 91 | "additionalProperties": false 92 | }, 93 | "FieldChoiceWithLabelSchema": { 94 | "id": "/FieldChoiceWithLabelSchema", 95 | "description": 96 | "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)).", 97 | "type": "object", 98 | "required": ["value", "sample", "label"], 99 | "properties": { 100 | "value": { 101 | "description": 102 | "The actual value that is sent into the Zap. Should match sample exactly.", 103 | "type": "string", 104 | "minLength": 1 105 | }, 106 | "sample": { 107 | "description": 108 | "Displayed as light grey text in the editor. It's important that the value match the sample. Otherwise, the actual value won't match what the user picked, which is confusing.", 109 | "type": "string", 110 | "minLength": 1 111 | }, 112 | "label": { 113 | "description": "A human readable label for this value.", 114 | "type": "string", 115 | "minLength": 1 116 | } 117 | } 118 | }, 119 | "RefResourceSchema": { 120 | "id": "/RefResourceSchema", 121 | "description": 122 | "Reference a resource by key and the data it returns. In the format of: `{resource_key}.{foreign_key}(.{human_label_key})`.", 123 | "type": "string", 124 | "pattern": "^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)?(\\.[a-zA-Z0-9_]+)?$" 125 | }, 126 | "FieldChoicesSchema": { 127 | "id": "/FieldChoicesSchema", 128 | "description": 129 | "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)", 130 | "oneOf": [ 131 | { 132 | "type": "object", 133 | "minProperties": 1 134 | }, 135 | { 136 | "type": "array", 137 | "minItems": 1, 138 | "items": { 139 | "oneOf": [ 140 | { 141 | "type": "string" 142 | }, 143 | { 144 | "$ref": "/FieldChoiceWithLabelSchema" 145 | } 146 | ] 147 | } 148 | } 149 | ] 150 | }, 151 | "FieldSchema": { 152 | "id": "/FieldSchema", 153 | "description": 154 | "Defines a field an app either needs as input, or gives as output. In addition to the requirements below, the following keys are mutually exclusive:\n\n* `children` & `list`\n* `children` & `dict`\n* `children` & `type`\n* `children` & `placeholder`\n* `children` & `helpText`\n* `children` & `default`\n* `dict` & `list`\n* `dynamic` & `dict`\n* `dynamic` & `choices`", 155 | "type": "object", 156 | "required": ["key"], 157 | "properties": { 158 | "key": { 159 | "description": 160 | "A unique machine readable key for this value (IE: \"fname\").", 161 | "type": "string", 162 | "minLength": 1 163 | }, 164 | "label": { 165 | "description": 166 | "A human readable label for this value (IE: \"First Name\").", 167 | "type": "string", 168 | "minLength": 1 169 | }, 170 | "helpText": { 171 | "description": 172 | "A human readable description of this value (IE: \"The first part of a full name.\"). You can use Markdown.", 173 | "type": "string", 174 | "minLength": 1, 175 | "maxLength": 1000 176 | }, 177 | "type": { 178 | "description": "The type of this value.", 179 | "type": "string", 180 | "enum": [ 181 | "string", 182 | "text", 183 | "integer", 184 | "number", 185 | "boolean", 186 | "datetime", 187 | "file", 188 | "password", 189 | "copy" 190 | ] 191 | }, 192 | "required": { 193 | "description": "If this value is required or not.", 194 | "type": "boolean" 195 | }, 196 | "placeholder": { 197 | "description": "An example value that is not saved.", 198 | "type": "string", 199 | "minLength": 1 200 | }, 201 | "default": { 202 | "description": 203 | "A default value that is saved the first time a Zap is created.", 204 | "type": "string", 205 | "minLength": 1 206 | }, 207 | "dynamic": { 208 | "description": 209 | "A reference to a trigger that will power a dynamic dropdown.", 210 | "$ref": "/RefResourceSchema" 211 | }, 212 | "search": { 213 | "description": 214 | "A reference to a search that will guide the user to add a search step to populate this field when creating a Zap.", 215 | "$ref": "/RefResourceSchema" 216 | }, 217 | "choices": { 218 | "description": 219 | "An object of machine keys and human values to populate a static dropdown.", 220 | "$ref": "/FieldChoicesSchema" 221 | }, 222 | "list": { 223 | "description": "Can a user provide multiples of this field?", 224 | "type": "boolean" 225 | }, 226 | "children": { 227 | "type": "array", 228 | "items": { 229 | "$ref": "/FieldSchema" 230 | }, 231 | "description": 232 | "An array of child fields that define the structure of a sub-object for this field. Usually used for line items.", 233 | "minItems": 1 234 | }, 235 | "dict": { 236 | "description": "Is this field a key/value input?", 237 | "type": "boolean" 238 | }, 239 | "computed": { 240 | "description": 241 | "Is this field automatically populated (and hidden from the user)?", 242 | "type": "boolean" 243 | }, 244 | "altersDynamicFields": { 245 | "description": 246 | "Does the value of this field affect the definitions of other fields in the set?", 247 | "type": "boolean" 248 | }, 249 | "inputFormat": { 250 | "description": 251 | "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\").", 252 | "type": "string", 253 | "pattern": "^.*{{input}}.*$" 254 | } 255 | }, 256 | "additionalProperties": false 257 | }, 258 | "FunctionRequireSchema": { 259 | "id": "/FunctionRequireSchema", 260 | "description": 261 | "A path to a file that might have content like `module.exports = (z, bundle) => [{id: 123}];`.", 262 | "type": "object", 263 | "additionalProperties": false, 264 | "required": ["require"], 265 | "properties": { 266 | "require": { 267 | "type": "string" 268 | } 269 | } 270 | }, 271 | "FunctionSourceSchema": { 272 | "id": "/FunctionSourceSchema", 273 | "description": 274 | "Source code like `{source: \"return 1 + 2\"}` which the system will wrap in a function for you.", 275 | "type": "object", 276 | "additionalProperties": false, 277 | "required": ["source"], 278 | "properties": { 279 | "source": { 280 | "type": "string", 281 | "pattern": "return", 282 | "description": 283 | "JavaScript code for the function body. This must end with a `return` statement." 284 | }, 285 | "args": { 286 | "type": "array", 287 | "items": { 288 | "type": "string" 289 | }, 290 | "description": 291 | "Function signature. Defaults to `['z', 'bundle']` if not specified." 292 | } 293 | } 294 | }, 295 | "FlatObjectSchema": { 296 | "id": "/FlatObjectSchema", 297 | "description": "An object whose values can only be primitives", 298 | "type": "object", 299 | "patternProperties": { 300 | "[^\\s]+": { 301 | "description": 302 | "Any key may exist in this flat object as long as its values are simple.", 303 | "anyOf": [ 304 | { 305 | "type": "null" 306 | }, 307 | { 308 | "type": "string" 309 | }, 310 | { 311 | "type": "integer" 312 | }, 313 | { 314 | "type": "number" 315 | }, 316 | { 317 | "type": "boolean" 318 | } 319 | ] 320 | } 321 | }, 322 | "additionalProperties": false 323 | }, 324 | "FunctionSchema": { 325 | "id": "/FunctionSchema", 326 | "description": 327 | "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.", 328 | "oneOf": [ 329 | { 330 | "type": "string", 331 | "pattern": "^\\$func\\$\\d+\\$[tf]\\$$" 332 | }, 333 | { 334 | "$ref": "/FunctionRequireSchema" 335 | }, 336 | { 337 | "$ref": "/FunctionSourceSchema" 338 | } 339 | ] 340 | }, 341 | "RedirectRequestSchema": { 342 | "id": "/RedirectRequestSchema", 343 | "description": 344 | "A representation of a HTTP redirect - you can use the `{{syntax}}` to inject authentication, field or global variables.", 345 | "type": "object", 346 | "properties": { 347 | "method": { 348 | "description": "The HTTP method for the request.", 349 | "type": "string", 350 | "default": "GET", 351 | "enum": ["GET"] 352 | }, 353 | "url": { 354 | "description": 355 | "A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.", 356 | "type": "string" 357 | }, 358 | "params": { 359 | "description": 360 | "A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.", 361 | "$ref": "/FlatObjectSchema" 362 | } 363 | }, 364 | "additionalProperties": false 365 | }, 366 | "RequestSchema": { 367 | "id": "/RequestSchema", 368 | "description": 369 | "A representation of a HTTP request - you can use the `{{syntax}}` to inject authentication, field or global variables.", 370 | "type": "object", 371 | "properties": { 372 | "method": { 373 | "description": "The HTTP method for the request.", 374 | "type": "string", 375 | "default": "GET", 376 | "enum": ["GET", "PUT", "POST", "PATCH", "DELETE", "HEAD"] 377 | }, 378 | "url": { 379 | "description": 380 | "A URL for the request (we will parse the querystring and merge with params). Keys and values will not be re-encoded.", 381 | "type": "string" 382 | }, 383 | "body": { 384 | "description": 385 | "Can be nothing, a raw string or JSON (object or array).", 386 | "oneOf": [ 387 | { 388 | "type": "null" 389 | }, 390 | { 391 | "type": "string" 392 | }, 393 | { 394 | "type": "object" 395 | }, 396 | { 397 | "type": "array" 398 | } 399 | ] 400 | }, 401 | "params": { 402 | "description": 403 | "A mapping of the querystring - will get merged with any query params in the URL. Keys and values will be encoded.", 404 | "$ref": "/FlatObjectSchema" 405 | }, 406 | "headers": { 407 | "description": "The HTTP headers for the request.", 408 | "$ref": "/FlatObjectSchema" 409 | }, 410 | "auth": { 411 | "description": 412 | "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']`.", 413 | "oneOf": [ 414 | { 415 | "type": "array", 416 | "items": { 417 | "type": "string", 418 | "minProperties": 2, 419 | "maxProperties": 2 420 | } 421 | }, 422 | { 423 | "$ref": "/FlatObjectSchema" 424 | } 425 | ] 426 | }, 427 | "removeMissingValuesFrom": { 428 | "description": 429 | "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 }```", 430 | "type": "object", 431 | "properties": { 432 | "params": { 433 | "description": 434 | "Refers to data sent via a requests query params (`req.params`)", 435 | "type": "boolean", 436 | "default": false 437 | }, 438 | "body": { 439 | "description": 440 | "Refers to tokens sent via a requsts body (`req.body`)", 441 | "type": "boolean", 442 | "default": false 443 | } 444 | }, 445 | "additionalProperties": false 446 | } 447 | }, 448 | "additionalProperties": false 449 | }, 450 | "FieldsSchema": { 451 | "id": "/FieldsSchema", 452 | "description": "An array or collection of fields.", 453 | "type": "array", 454 | "items": { 455 | "$ref": "/FieldSchema" 456 | } 457 | }, 458 | "AuthenticationBasicConfigSchema": { 459 | "id": "/AuthenticationBasicConfigSchema", 460 | "description": 461 | "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.", 462 | "type": "object", 463 | "properties": {}, 464 | "additionalProperties": false 465 | }, 466 | "AuthenticationCustomConfigSchema": { 467 | "id": "/AuthenticationCustomConfigSchema", 468 | "description": 469 | "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.", 470 | "type": "object", 471 | "properties": {}, 472 | "additionalProperties": false 473 | }, 474 | "AuthenticationDigestConfigSchema": { 475 | "id": "/AuthenticationDigestConfigSchema", 476 | "description": 477 | "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.", 478 | "type": "object", 479 | "properties": {}, 480 | "additionalProperties": false 481 | }, 482 | "AuthenticationOAuth1ConfigSchema": { 483 | "id": "/AuthenticationOAuth1ConfigSchema", 484 | "description": "Config for OAuth1 authentication.", 485 | "type": "object", 486 | "required": ["getRequestToken", "authorizeUrl", "getAccessToken"], 487 | "properties": { 488 | "getRequestToken": { 489 | "description": 490 | "Define where Zapier will acquire a request token which is used for the rest of the three legged authentication process.", 491 | "oneOf": [ 492 | { 493 | "$ref": "/RequestSchema" 494 | }, 495 | { 496 | "$ref": "/FunctionSchema" 497 | } 498 | ] 499 | }, 500 | "authorizeUrl": { 501 | "description": 502 | "Define where Zapier will redirect the user to authorize our app. Typically, you should append an `oauth_token` querystring parameter to the request.", 503 | "oneOf": [ 504 | { 505 | "$ref": "/RedirectRequestSchema" 506 | }, 507 | { 508 | "$ref": "/FunctionSchema" 509 | } 510 | ] 511 | }, 512 | "getAccessToken": { 513 | "description": 514 | "Define how Zapier fetches an access token from the API", 515 | "oneOf": [ 516 | { 517 | "$ref": "/RequestSchema" 518 | }, 519 | { 520 | "$ref": "/FunctionSchema" 521 | } 522 | ] 523 | } 524 | }, 525 | "additionalProperties": false 526 | }, 527 | "AuthenticationOAuth2ConfigSchema": { 528 | "id": "/AuthenticationOAuth2ConfigSchema", 529 | "description": "Config for OAuth2 authentication.", 530 | "type": "object", 531 | "required": ["authorizeUrl", "getAccessToken"], 532 | "properties": { 533 | "authorizeUrl": { 534 | "description": 535 | "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.", 536 | "oneOf": [ 537 | { 538 | "$ref": "/RedirectRequestSchema" 539 | }, 540 | { 541 | "$ref": "/FunctionSchema" 542 | } 543 | ] 544 | }, 545 | "getAccessToken": { 546 | "description": 547 | "Define how Zapier fetches an access token from the API", 548 | "oneOf": [ 549 | { 550 | "$ref": "/RequestSchema" 551 | }, 552 | { 553 | "$ref": "/FunctionSchema" 554 | } 555 | ] 556 | }, 557 | "refreshAccessToken": { 558 | "description": 559 | "Define how Zapier will refresh the access token from the API", 560 | "oneOf": [ 561 | { 562 | "$ref": "/RequestSchema" 563 | }, 564 | { 565 | "$ref": "/FunctionSchema" 566 | } 567 | ] 568 | }, 569 | "scope": { 570 | "description": "What scope should Zapier request?", 571 | "type": "string" 572 | }, 573 | "autoRefresh": { 574 | "description": 575 | "Should Zapier include a pre-built afterResponse middleware that invokes `refreshAccessToken` when we receive a 401 response?", 576 | "type": "boolean" 577 | } 578 | }, 579 | "additionalProperties": false 580 | }, 581 | "AuthenticationSessionConfigSchema": { 582 | "id": "/AuthenticationSessionConfigSchema", 583 | "description": "Config for session authentication.", 584 | "type": "object", 585 | "required": ["perform"], 586 | "properties": { 587 | "perform": { 588 | "description": 589 | "Define how Zapier fetches the additional authData needed to make API calls.", 590 | "oneOf": [ 591 | { 592 | "$ref": "/RequestSchema" 593 | }, 594 | { 595 | "$ref": "/FunctionSchema" 596 | } 597 | ] 598 | } 599 | }, 600 | "additionalProperties": false 601 | }, 602 | "FieldOrFunctionSchema": { 603 | "id": "/FieldOrFunctionSchema", 604 | "description": "Represents an array of fields or functions.", 605 | "type": "array", 606 | "items": { 607 | "oneOf": [ 608 | { 609 | "$ref": "/FieldSchema" 610 | }, 611 | { 612 | "$ref": "/FunctionSchema" 613 | } 614 | ] 615 | } 616 | }, 617 | "DynamicFieldsSchema": { 618 | "id": "/DynamicFieldsSchema", 619 | "description": 620 | "Like [/FieldsSchema](#fieldsschema) but you can provide functions to create dynamic or custom fields.", 621 | "$ref": "/FieldOrFunctionSchema" 622 | }, 623 | "ResultsSchema": { 624 | "id": "/ResultsSchema", 625 | "description": 626 | "An array of objects suitable for returning in perform calls.", 627 | "type": "array", 628 | "items": { 629 | "type": "object", 630 | "minProperties": 1 631 | } 632 | }, 633 | "BasicDisplaySchema": { 634 | "id": "/BasicDisplaySchema", 635 | "description": 636 | "Represents user information for a trigger, search, or create.", 637 | "type": "object", 638 | "required": ["label", "description"], 639 | "properties": { 640 | "label": { 641 | "description": 642 | "A short label like \"New Record\" or \"Create Record in Project\".", 643 | "type": "string", 644 | "minLength": 2, 645 | "maxLength": 64 646 | }, 647 | "description": { 648 | "description": 649 | "A description of what this trigger, search, or create does.", 650 | "type": "string", 651 | "minLength": 1, 652 | "maxLength": 1000 653 | }, 654 | "directions": { 655 | "description": 656 | "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.", 657 | "type": "string", 658 | "minLength": 12, 659 | "maxLength": 1000 660 | }, 661 | "important": { 662 | "description": 663 | "Affects how prominently this operation is displayed in the UI. Only mark a few of the most popular operations important.", 664 | "type": "boolean" 665 | }, 666 | "hidden": { 667 | "description": "Should this operation be unselectable by users?", 668 | "type": "boolean" 669 | } 670 | }, 671 | "additionalProperties": false 672 | }, 673 | "BasicOperationSchema": { 674 | "id": "/BasicOperationSchema", 675 | "description": 676 | "Represents the fundamental mechanics of triggers, searches, or creates.", 677 | "type": "object", 678 | "required": ["perform"], 679 | "properties": { 680 | "resource": { 681 | "description": 682 | "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.", 683 | "$ref": "/RefResourceSchema" 684 | }, 685 | "perform": { 686 | "description": 687 | "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.", 688 | "oneOf": [ 689 | { 690 | "$ref": "/RequestSchema" 691 | }, 692 | { 693 | "$ref": "/FunctionSchema" 694 | } 695 | ] 696 | }, 697 | "inputFields": { 698 | "description": 699 | "What should the form a user sees and configures look like?", 700 | "$ref": "/DynamicFieldsSchema" 701 | }, 702 | "outputFields": { 703 | "description": 704 | "What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.", 705 | "$ref": "/DynamicFieldsSchema" 706 | }, 707 | "sample": { 708 | "description": 709 | "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", 710 | "type": "object", 711 | "minProperties": 1, 712 | "docAnnotation": { 713 | "required": { 714 | "type": "replace", 715 | "value": "**yes** (with exceptions, see description)" 716 | } 717 | } 718 | } 719 | }, 720 | "additionalProperties": false 721 | }, 722 | "BasicHookOperationSchema": { 723 | "id": "/BasicHookOperationSchema", 724 | "description": 725 | "Represents the inbound mechanics of hooks with optional subscribe/unsubscribe. Defers to list for fields.", 726 | "type": "object", 727 | "required": ["perform"], 728 | "properties": { 729 | "type": { 730 | "description": 731 | "Must be explicitly set to `\"hook\"` unless this hook is defined as part of a resource, in which case it's optional.", 732 | "type": "string", 733 | "enum": ["hook"], 734 | "docAnnotation": { 735 | "required": { 736 | "type": "replace", 737 | "value": "**yes** (with exceptions, see description)" 738 | } 739 | } 740 | }, 741 | "resource": { 742 | "description": 743 | "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.", 744 | "$ref": "/RefResourceSchema" 745 | }, 746 | "perform": { 747 | "description": 748 | "A function that processes the inbound webhook request.", 749 | "$ref": "/FunctionSchema" 750 | }, 751 | "performList": { 752 | "description": 753 | "Can get \"live\" data on demand instead of waiting for a hook. If you find yourself reaching for this - consider resources and their built-in hook/list methods. Note: this is required for public apps to ensure the best UX for the end-user. For private apps, you can ignore warnings about this property with the `--without-style` flag during `zapier push`.", 754 | "oneOf": [ 755 | { 756 | "$ref": "/RequestSchema" 757 | }, 758 | { 759 | "$ref": "/FunctionSchema" 760 | } 761 | ], 762 | "docAnnotation": { 763 | "required": { 764 | "type": "replace", 765 | "value": "**yes** (with exceptions, see description)" 766 | } 767 | } 768 | }, 769 | "performSubscribe": { 770 | "description": 771 | "Takes a URL and any necessary data from the user and subscribes. Note: this is required for public apps to ensure the best UX for the end-user. For private apps, you can ignore warnings about this property with the `--without-style` flag during `zapier push`.", 772 | "oneOf": [ 773 | { 774 | "$ref": "/RequestSchema" 775 | }, 776 | { 777 | "$ref": "/FunctionSchema" 778 | } 779 | ], 780 | "docAnnotation": { 781 | "required": { 782 | "type": "replace", 783 | "value": "**yes** (with exceptions, see description)" 784 | } 785 | } 786 | }, 787 | "performUnsubscribe": { 788 | "description": 789 | "Takes a URL and data from a previous subscribe call and unsubscribes. Note: this is required for public apps to ensure the best UX for the end-user. For private apps, you can ignore warnings about this property with the `--without-style` flag during `zapier push`.", 790 | "oneOf": [ 791 | { 792 | "$ref": "/RequestSchema" 793 | }, 794 | { 795 | "$ref": "/FunctionSchema" 796 | } 797 | ], 798 | "docAnnotation": { 799 | "required": { 800 | "type": "replace", 801 | "value": "**yes** (with exceptions, see description)" 802 | } 803 | } 804 | }, 805 | "inputFields": { 806 | "description": 807 | "What should the form a user sees and configures look like?", 808 | "$ref": "/DynamicFieldsSchema" 809 | }, 810 | "outputFields": { 811 | "description": 812 | "What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.", 813 | "$ref": "/DynamicFieldsSchema" 814 | }, 815 | "sample": { 816 | "description": 817 | "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", 818 | "type": "object", 819 | "minProperties": 1, 820 | "docAnnotation": { 821 | "required": { 822 | "type": "replace", 823 | "value": "**yes** (with exceptions, see description)" 824 | } 825 | } 826 | } 827 | }, 828 | "additionalProperties": false 829 | }, 830 | "BasicPollingOperationSchema": { 831 | "id": "/BasicPollingOperationSchema", 832 | "description": "Represents the fundamental mechanics of a trigger.", 833 | "type": "object", 834 | "required": ["perform"], 835 | "properties": { 836 | "type": { 837 | "description": 838 | "Clarify how this operation works (polling == pull or hook == push).", 839 | "type": "string", 840 | "default": "polling", 841 | "enum": ["polling"] 842 | }, 843 | "resource": { 844 | "description": 845 | "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.", 846 | "$ref": "/RefResourceSchema" 847 | }, 848 | "perform": { 849 | "description": 850 | "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.", 851 | "oneOf": [ 852 | { 853 | "$ref": "/RequestSchema" 854 | }, 855 | { 856 | "$ref": "/FunctionSchema" 857 | } 858 | ] 859 | }, 860 | "canPaginate": { 861 | "description": "Does this endpoint support a page offset?", 862 | "type": "boolean" 863 | }, 864 | "inputFields": { 865 | "description": 866 | "What should the form a user sees and configures look like?", 867 | "$ref": "/DynamicFieldsSchema" 868 | }, 869 | "outputFields": { 870 | "description": 871 | "What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.", 872 | "$ref": "/DynamicFieldsSchema" 873 | }, 874 | "sample": { 875 | "description": 876 | "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", 877 | "type": "object", 878 | "minProperties": 1, 879 | "docAnnotation": { 880 | "required": { 881 | "type": "replace", 882 | "value": "**yes** (with exceptions, see description)" 883 | } 884 | } 885 | } 886 | }, 887 | "additionalProperties": false 888 | }, 889 | "BasicActionOperationSchema": { 890 | "id": "/BasicActionOperationSchema", 891 | "description": "Represents the fundamental mechanics of a search/create.", 892 | "type": "object", 893 | "required": ["perform"], 894 | "properties": { 895 | "resource": { 896 | "description": 897 | "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.", 898 | "$ref": "/RefResourceSchema" 899 | }, 900 | "perform": { 901 | "description": 902 | "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.", 903 | "oneOf": [ 904 | { 905 | "$ref": "/RequestSchema" 906 | }, 907 | { 908 | "$ref": "/FunctionSchema" 909 | } 910 | ] 911 | }, 912 | "performResume": { 913 | "description": 914 | "A function that parses data from a perform + callback to resume this action. For use with callback semantics", 915 | "$ref": "/FunctionSchema" 916 | }, 917 | "performGet": { 918 | "description": 919 | "How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.", 920 | "oneOf": [ 921 | { 922 | "$ref": "/RequestSchema" 923 | }, 924 | { 925 | "$ref": "/FunctionSchema" 926 | } 927 | ] 928 | }, 929 | "inputFields": { 930 | "description": 931 | "What should the form a user sees and configures look like?", 932 | "$ref": "/DynamicFieldsSchema" 933 | }, 934 | "outputFields": { 935 | "description": 936 | "What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.", 937 | "$ref": "/DynamicFieldsSchema" 938 | }, 939 | "sample": { 940 | "description": 941 | "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", 942 | "type": "object", 943 | "minProperties": 1, 944 | "docAnnotation": { 945 | "required": { 946 | "type": "replace", 947 | "value": "**yes** (with exceptions, see description)" 948 | } 949 | } 950 | } 951 | }, 952 | "additionalProperties": false 953 | }, 954 | "ResourceMethodGetSchema": { 955 | "id": "/ResourceMethodGetSchema", 956 | "description": 957 | "How will we get a single object given a unique identifier/id?", 958 | "type": "object", 959 | "required": ["display", "operation"], 960 | "properties": { 961 | "display": { 962 | "description": 963 | "Define how this get method will be exposed in the UI.", 964 | "$ref": "/BasicDisplaySchema" 965 | }, 966 | "operation": { 967 | "description": "Define how this get method will work.", 968 | "$ref": "/BasicOperationSchema" 969 | } 970 | }, 971 | "additionalProperties": false 972 | }, 973 | "ResourceMethodHookSchema": { 974 | "id": "/ResourceMethodHookSchema", 975 | "description": 976 | "How will we get notified of new objects? Will be turned into a trigger automatically.", 977 | "type": "object", 978 | "required": ["display", "operation"], 979 | "properties": { 980 | "display": { 981 | "description": 982 | "Define how this hook/trigger method will be exposed in the UI.", 983 | "$ref": "/BasicDisplaySchema" 984 | }, 985 | "operation": { 986 | "description": "Define how this hook/trigger method will work.", 987 | "$ref": "/BasicHookOperationSchema" 988 | } 989 | }, 990 | "additionalProperties": false 991 | }, 992 | "ResourceMethodListSchema": { 993 | "id": "/ResourceMethodListSchema", 994 | "description": 995 | "How will we get a list of new objects? Will be turned into a trigger automatically.", 996 | "type": "object", 997 | "required": ["display", "operation"], 998 | "properties": { 999 | "display": { 1000 | "description": 1001 | "Define how this list/trigger method will be exposed in the UI.", 1002 | "$ref": "/BasicDisplaySchema" 1003 | }, 1004 | "operation": { 1005 | "description": "Define how this list/trigger method will work.", 1006 | "$ref": "/BasicPollingOperationSchema" 1007 | } 1008 | }, 1009 | "additionalProperties": false 1010 | }, 1011 | "ResourceMethodSearchSchema": { 1012 | "id": "/ResourceMethodSearchSchema", 1013 | "description": 1014 | "How will we find a specific object given filters or search terms? Will be turned into a search automatically.", 1015 | "type": "object", 1016 | "required": ["display", "operation"], 1017 | "properties": { 1018 | "display": { 1019 | "description": 1020 | "Define how this search method will be exposed in the UI.", 1021 | "$ref": "/BasicDisplaySchema" 1022 | }, 1023 | "operation": { 1024 | "description": "Define how this search method will work.", 1025 | "$ref": "/BasicActionOperationSchema" 1026 | } 1027 | }, 1028 | "additionalProperties": false 1029 | }, 1030 | "ResourceMethodCreateSchema": { 1031 | "id": "/ResourceMethodCreateSchema", 1032 | "description": 1033 | "How will we find create a specific object given inputs? Will be turned into a create automatically.", 1034 | "type": "object", 1035 | "required": ["display", "operation"], 1036 | "properties": { 1037 | "display": { 1038 | "description": 1039 | "Define how this create method will be exposed in the UI.", 1040 | "$ref": "/BasicDisplaySchema" 1041 | }, 1042 | "operation": { 1043 | "description": "Define how this create method will work.", 1044 | "$ref": "/BasicActionOperationSchema" 1045 | } 1046 | }, 1047 | "additionalProperties": false 1048 | }, 1049 | "KeySchema": { 1050 | "id": "/KeySchema", 1051 | "description": "A unique identifier for this item.", 1052 | "type": "string", 1053 | "minLength": 2, 1054 | "pattern": "^[a-zA-Z]+[a-zA-Z0-9_]*$" 1055 | }, 1056 | "ResourceSchema": { 1057 | "id": "/ResourceSchema", 1058 | "description": 1059 | "Represents a resource, which will in turn power triggers, searches, or creates.", 1060 | "type": "object", 1061 | "required": ["key", "noun"], 1062 | "properties": { 1063 | "key": { 1064 | "description": "A key to uniquely identify this resource.", 1065 | "$ref": "/KeySchema" 1066 | }, 1067 | "noun": { 1068 | "description": 1069 | "A noun for this resource that completes the sentence \"create a new XXX\".", 1070 | "type": "string", 1071 | "minLength": 2, 1072 | "maxLength": 255 1073 | }, 1074 | "get": { 1075 | "description": 1076 | "How will we get a single object given a unique identifier/id?", 1077 | "$ref": "/ResourceMethodGetSchema" 1078 | }, 1079 | "hook": { 1080 | "description": 1081 | "How will we get notified of new objects? Will be turned into a trigger automatically.", 1082 | "$ref": "/ResourceMethodHookSchema" 1083 | }, 1084 | "list": { 1085 | "description": 1086 | "How will we get a list of new objects? Will be turned into a trigger automatically.", 1087 | "$ref": "/ResourceMethodListSchema" 1088 | }, 1089 | "search": { 1090 | "description": 1091 | "How will we find a specific object given filters or search terms? Will be turned into a search automatically.", 1092 | "$ref": "/ResourceMethodSearchSchema" 1093 | }, 1094 | "create": { 1095 | "description": 1096 | "How will we find create a specific object given inputs? Will be turned into a create automatically.", 1097 | "$ref": "/ResourceMethodCreateSchema" 1098 | }, 1099 | "outputFields": { 1100 | "description": "What fields of data will this return?", 1101 | "$ref": "/DynamicFieldsSchema" 1102 | }, 1103 | "sample": { 1104 | "description": "What does a sample of data look like?", 1105 | "type": "object", 1106 | "minProperties": 1 1107 | } 1108 | }, 1109 | "additionalProperties": false 1110 | }, 1111 | "TriggerSchema": { 1112 | "id": "/TriggerSchema", 1113 | "description": "How will Zapier get notified of new objects?", 1114 | "type": "object", 1115 | "required": ["key", "noun", "display", "operation"], 1116 | "properties": { 1117 | "key": { 1118 | "description": "A key to uniquely identify this trigger.", 1119 | "$ref": "/KeySchema" 1120 | }, 1121 | "noun": { 1122 | "description": 1123 | "A noun for this trigger that completes the sentence \"triggers on a new XXX\".", 1124 | "type": "string", 1125 | "minLength": 2, 1126 | "maxLength": 255 1127 | }, 1128 | "display": { 1129 | "description": "Configures the UI for this trigger.", 1130 | "$ref": "/BasicDisplaySchema" 1131 | }, 1132 | "operation": { 1133 | "description": "Powers the functionality for this trigger.", 1134 | "anyOf": [ 1135 | { 1136 | "$ref": "/BasicPollingOperationSchema" 1137 | }, 1138 | { 1139 | "$ref": "/BasicHookOperationSchema" 1140 | } 1141 | ] 1142 | } 1143 | }, 1144 | "additionalProperties": false 1145 | }, 1146 | "SearchSchema": { 1147 | "id": "/SearchSchema", 1148 | "description": "How will Zapier search for existing objects?", 1149 | "type": "object", 1150 | "required": ["key", "noun", "display", "operation"], 1151 | "properties": { 1152 | "key": { 1153 | "description": "A key to uniquely identify this search.", 1154 | "$ref": "/KeySchema" 1155 | }, 1156 | "noun": { 1157 | "description": 1158 | "A noun for this search that completes the sentence \"finds a specific XXX\".", 1159 | "type": "string", 1160 | "minLength": 2, 1161 | "maxLength": 255 1162 | }, 1163 | "display": { 1164 | "description": "Configures the UI for this search.", 1165 | "$ref": "/BasicDisplaySchema" 1166 | }, 1167 | "operation": { 1168 | "description": "Powers the functionality for this search.", 1169 | "$ref": "/BasicActionOperationSchema" 1170 | } 1171 | }, 1172 | "additionalProperties": false 1173 | }, 1174 | "BasicCreateActionOperationSchema": { 1175 | "id": "/BasicCreateActionOperationSchema", 1176 | "description": "Represents the fundamental mechanics of a create.", 1177 | "type": "object", 1178 | "required": ["perform"], 1179 | "properties": { 1180 | "resource": { 1181 | "description": 1182 | "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.", 1183 | "$ref": "/RefResourceSchema" 1184 | }, 1185 | "perform": { 1186 | "description": 1187 | "How will Zapier get the data? This can be a function like `(z) => [{id: 123}]` or a request like `{url: 'http...'}`.", 1188 | "oneOf": [ 1189 | { 1190 | "$ref": "/RequestSchema" 1191 | }, 1192 | { 1193 | "$ref": "/FunctionSchema" 1194 | } 1195 | ] 1196 | }, 1197 | "performResume": { 1198 | "description": 1199 | "A function that parses data from a perform + callback to resume this action. For use with callback semantics", 1200 | "$ref": "/FunctionSchema" 1201 | }, 1202 | "performGet": { 1203 | "description": 1204 | "How will Zapier get a single record? If you find yourself reaching for this - consider resources and their built-in get methods.", 1205 | "oneOf": [ 1206 | { 1207 | "$ref": "/RequestSchema" 1208 | }, 1209 | { 1210 | "$ref": "/FunctionSchema" 1211 | } 1212 | ] 1213 | }, 1214 | "inputFields": { 1215 | "description": 1216 | "What should the form a user sees and configures look like?", 1217 | "$ref": "/DynamicFieldsSchema" 1218 | }, 1219 | "outputFields": { 1220 | "description": 1221 | "What fields of data will this return? Will use resource outputFields if missing, will also use sample if available.", 1222 | "$ref": "/DynamicFieldsSchema" 1223 | }, 1224 | "sample": { 1225 | "description": 1226 | "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", 1227 | "type": "object", 1228 | "minProperties": 1, 1229 | "docAnnotation": { 1230 | "required": { 1231 | "type": "replace", 1232 | "value": "**yes** (with exceptions, see description)" 1233 | } 1234 | } 1235 | }, 1236 | "shouldLock": { 1237 | "description": 1238 | "Should this action be performed one at a time (avoid concurrency)?", 1239 | "type": "boolean" 1240 | } 1241 | }, 1242 | "additionalProperties": false 1243 | }, 1244 | "CreateSchema": { 1245 | "id": "/CreateSchema", 1246 | "description": "How will Zapier create a new object?", 1247 | "type": "object", 1248 | "required": ["key", "noun", "display", "operation"], 1249 | "properties": { 1250 | "key": { 1251 | "description": "A key to uniquely identify this create.", 1252 | "$ref": "/KeySchema" 1253 | }, 1254 | "noun": { 1255 | "description": 1256 | "A noun for this create that completes the sentence \"creates a new XXX\".", 1257 | "type": "string", 1258 | "minLength": 2, 1259 | "maxLength": 255 1260 | }, 1261 | "display": { 1262 | "description": "Configures the UI for this create.", 1263 | "$ref": "/BasicDisplaySchema" 1264 | }, 1265 | "operation": { 1266 | "description": "Powers the functionality for this create.", 1267 | "$ref": "/BasicCreateActionOperationSchema" 1268 | } 1269 | }, 1270 | "additionalProperties": false 1271 | }, 1272 | "SearchOrCreateSchema": { 1273 | "id": "/SearchOrCreateSchema", 1274 | "description": 1275 | "Pair an existing search and a create to enable \"Find or Create\" functionality in your app", 1276 | "type": "object", 1277 | "required": ["key", "display", "search", "create"], 1278 | "properties": { 1279 | "key": { 1280 | "description": 1281 | "A key to uniquely identify this search-or-create. Must match the search key.", 1282 | "$ref": "/KeySchema" 1283 | }, 1284 | "display": { 1285 | "description": "Configures the UI for this search-or-create.", 1286 | "$ref": "/BasicDisplaySchema" 1287 | }, 1288 | "search": { 1289 | "description": 1290 | "The key of the search that powers this search-or-create", 1291 | "$ref": "/KeySchema" 1292 | }, 1293 | "create": { 1294 | "description": 1295 | "The key of the create that powers this search-or-create", 1296 | "$ref": "/KeySchema" 1297 | } 1298 | }, 1299 | "additionalProperties": false 1300 | }, 1301 | "AuthenticationSchema": { 1302 | "id": "/AuthenticationSchema", 1303 | "description": "Represents authentication schemes.", 1304 | "type": "object", 1305 | "required": ["type", "test"], 1306 | "properties": { 1307 | "type": { 1308 | "description": "Choose which scheme you want to use.", 1309 | "type": "string", 1310 | "enum": ["basic", "custom", "digest", "oauth1", "oauth2", "session"] 1311 | }, 1312 | "test": { 1313 | "description": 1314 | "A function or request that confirms the authentication is working.", 1315 | "oneOf": [ 1316 | { 1317 | "$ref": "/RequestSchema" 1318 | }, 1319 | { 1320 | "$ref": "/FunctionSchema" 1321 | } 1322 | ] 1323 | }, 1324 | "fields": { 1325 | "description": 1326 | "Fields you can request from the user before they connect your app to Zapier.", 1327 | "$ref": "/FieldsSchema" 1328 | }, 1329 | "connectionLabel": { 1330 | "description": 1331 | "A string with variables, function, or request that returns the connection label for the authenticated user.", 1332 | "anyOf": [ 1333 | { 1334 | "$ref": "/RequestSchema" 1335 | }, 1336 | { 1337 | "$ref": "/FunctionSchema" 1338 | }, 1339 | { 1340 | "type": "string" 1341 | } 1342 | ] 1343 | }, 1344 | "basicConfig": { 1345 | "$ref": "/AuthenticationBasicConfigSchema" 1346 | }, 1347 | "customConfig": { 1348 | "$ref": "/AuthenticationCustomConfigSchema" 1349 | }, 1350 | "digestConfig": { 1351 | "$ref": "/AuthenticationDigestConfigSchema" 1352 | }, 1353 | "oauth1Config": { 1354 | "$ref": "/AuthenticationOAuth1ConfigSchema" 1355 | }, 1356 | "oauth2Config": { 1357 | "$ref": "/AuthenticationOAuth2ConfigSchema" 1358 | }, 1359 | "sessionConfig": { 1360 | "$ref": "/AuthenticationSessionConfigSchema" 1361 | } 1362 | }, 1363 | "additionalProperties": false 1364 | }, 1365 | "ResourcesSchema": { 1366 | "id": "/ResourcesSchema", 1367 | "description": 1368 | "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.", 1369 | "type": "object", 1370 | "patternProperties": { 1371 | "^[a-zA-Z]+[a-zA-Z0-9_]*$": { 1372 | "description": 1373 | "Any unique key can be used and its values will be validated against the ResourceSchema.", 1374 | "$ref": "/ResourceSchema" 1375 | } 1376 | }, 1377 | "additionalProperties": false 1378 | }, 1379 | "TriggersSchema": { 1380 | "id": "/TriggersSchema", 1381 | "description": 1382 | "Enumerates the triggers your app has available for users.", 1383 | "type": "object", 1384 | "patternProperties": { 1385 | "^[a-zA-Z]+[a-zA-Z0-9_]*$": { 1386 | "description": 1387 | "Any unique key can be used and its values will be validated against the TriggerSchema.", 1388 | "$ref": "/TriggerSchema" 1389 | } 1390 | }, 1391 | "additionalProperties": false 1392 | }, 1393 | "SearchesSchema": { 1394 | "id": "/SearchesSchema", 1395 | "description": 1396 | "Enumerates the searches your app has available for users.", 1397 | "type": "object", 1398 | "patternProperties": { 1399 | "^[a-zA-Z]+[a-zA-Z0-9_]*$": { 1400 | "description": 1401 | "Any unique key can be used and its values will be validated against the SearchSchema.", 1402 | "$ref": "/SearchSchema" 1403 | } 1404 | } 1405 | }, 1406 | "CreatesSchema": { 1407 | "id": "/CreatesSchema", 1408 | "description": "Enumerates the creates your app has available for users.", 1409 | "type": "object", 1410 | "patternProperties": { 1411 | "^[a-zA-Z]+[a-zA-Z0-9_]*$": { 1412 | "description": 1413 | "Any unique key can be used and its values will be validated against the CreateSchema.", 1414 | "$ref": "/CreateSchema" 1415 | } 1416 | } 1417 | }, 1418 | "SearchOrCreatesSchema": { 1419 | "id": "/SearchOrCreatesSchema", 1420 | "description": 1421 | "Enumerates the search-or-creates your app has available for users.", 1422 | "type": "object", 1423 | "patternProperties": { 1424 | "^[a-zA-Z]+[a-zA-Z0-9_]*$": { 1425 | "description": 1426 | "Any unique key can be used and its values will be validated against the SearchOrCreateSchema.", 1427 | "$ref": "/SearchOrCreateSchema" 1428 | } 1429 | } 1430 | }, 1431 | "VersionSchema": { 1432 | "id": "/VersionSchema", 1433 | "description": 1434 | "Represents a simplified semver string, from `0.0.0` to `999.999.999`.", 1435 | "type": "string", 1436 | "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$" 1437 | }, 1438 | "MiddlewaresSchema": { 1439 | "id": "/MiddlewaresSchema", 1440 | "description": 1441 | "List of before or after middlewares. Can be an array of functions or a single function", 1442 | "oneOf": [ 1443 | { 1444 | "type": "array", 1445 | "items": { 1446 | "$ref": "/FunctionSchema" 1447 | } 1448 | }, 1449 | { 1450 | "$ref": "/FunctionSchema" 1451 | } 1452 | ], 1453 | "additionalProperties": false 1454 | }, 1455 | "HydratorsSchema": { 1456 | "id": "/HydratorsSchema", 1457 | "description": 1458 | "A bank of named functions that you can use in `z.hydrate('someName')` to lazily load data.", 1459 | "type": "object", 1460 | "patternProperties": { 1461 | "^[a-zA-Z]+[a-zA-Z0-9]*$": { 1462 | "description": 1463 | "Any unique key can be used in `z.hydrate('uniqueKeyHere')`.", 1464 | "$ref": "/FunctionSchema" 1465 | } 1466 | }, 1467 | "additionalProperties": false 1468 | }, 1469 | "AppFlagsSchema": { 1470 | "id": "/AppFlagsSchema", 1471 | "description": "Codifies high-level options for your app.", 1472 | "type": "object", 1473 | "properties": { 1474 | "skipHttpPatch": { 1475 | "description": 1476 | "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).", 1477 | "type": "boolean" 1478 | } 1479 | }, 1480 | "additionalProperties": false 1481 | } 1482 | } 1483 | } 1484 | --------------------------------------------------------------------------------