UNPKG

119 kBMarkdownView Raw
1<!-- GENERATED! ONLY EDIT `README-source.md` -->
2
3<h1 align="center">
4 <a href="https://zapier.com"><img src="https://cdn.rawgit.com/zapier/zapier-platform-cli/master/goodies/zapier-logomark.png" alt="Zapier" width="200"></a>
5 <br>
6 Zapier Platform CLI
7 <br>
8 <br>
9</h1>
10
11<p align="center">
12 <a href="https://travis-ci.org/zapier/zapier-platform-cli"><img src="https://img.shields.io/travis/zapier/zapier-platform-cli/master.svg" alt="Travis"></a>
13 <a href="https://www.npmjs.com/package/zapier-platform-cli"><img src="https://img.shields.io/npm/v/zapier-platform-cli.svg" alt="npm version"></a>
14 <!--possible downloads badge too, once that's good-->
15</p>
16
17Zapier is a platform for creating integrations and workflows. This CLI is your gateway to creating custom applications on the Zapier platform.
18
19[These docs are available here](http://zapier.github.io/zapier-platform-cli/), the [CLI docs are available here](http://zapier.github.io/zapier-platform-cli/cli.html), and you can [view all the schema definitions here](https://zapier.github.io/zapier-platform-schema/build/schema.html).
20
21## Table of Contents
22
23<!-- toc -->
24
25- [Getting Started](#getting-started)
26 * [What is an App?](#what-is-an-app)
27 * [How does Zapier Platform CLI Work?](#how-does-zapier-platform-cli-work)
28 * [Zapier Platform CLI vs UI](#zapier-platform-cli-vs-ui)
29 * [Requirements](#requirements)
30 * [Quick Setup Guide](#quick-setup-guide)
31 * [Tutorial](#tutorial)
32- [Creating a Local App](#creating-a-local-app)
33 * [Local Project Structure](#local-project-structure)
34 * [Local App Definition](#local-app-definition)
35- [Registering an App](#registering-an-app)
36- [Deploying an App Version](#deploying-an-app-version)
37 * [Private App Version (default)](#private-app-version-default)
38 * [Sharing an App Version](#sharing-an-app-version)
39 * [Promoting an App Version](#promoting-an-app-version)
40- [Converting an Existing App](#converting-an-existing-app)
41- [Authentication](#authentication)
42 * [Basic](#basic)
43 * [Digest](#digest)
44 * [Custom](#custom)
45 * [Session](#session)
46 * [OAuth1](#oauth1)
47 * [OAuth2](#oauth2)
48- [Resources](#resources)
49 * [Resource Definition](#resource-definition)
50- [Triggers/Searches/Creates](#triggerssearchescreates)
51 * [Return Types](#return-types)
52- [Input Fields](#input-fields)
53 * [Custom/Dynamic Fields](#customdynamic-fields)
54 * [Dynamic Dropdowns](#dynamic-dropdowns)
55 * [Search-Powered Fields](#search-powered-fields)
56 * [Computed Fields](#computed-fields)
57 * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields)
58- [Output Fields](#output-fields)
59 * [Nested & Children (Line Item) Fields](#nested--children-line-item-fields-1)
60- [Z Object](#z-object)
61 * [`z.request([url], options)`](#zrequesturl-options)
62 * [`z.console`](#zconsole)
63 * [`z.dehydrate(func, inputData)`](#zdehydratefunc-inputdata)
64 * [`z.dehydrateFile(func, inputData)`](#zdehydratefilefunc-inputdata)
65 * [`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`](#zstashfilebufferstringstream-knownlength-filename-contenttype)
66 * [`z.JSON`](#zjson)
67 * [`z.hash()`](#zhash)
68 * [`z.errors`](#zerrors)
69 * [`z.cursor`](#zcursor)
70- [Bundle Object](#bundle-object)
71 * [`bundle.authData`](#bundleauthdata)
72 * [`bundle.inputData`](#bundleinputdata)
73 * [`bundle.inputDataRaw`](#bundleinputdataraw)
74 * [`bundle.meta`](#bundlemeta)
75 * [`bundle.rawRequest`](#bundlerawrequest)
76 * [`bundle.cleanedRequest`](#bundlecleanedrequest)
77 * [`bundle.targetUrl`](#bundletargeturl)
78 * [`bundle.subscribeData`](#bundlesubscribedata)
79- [Environment](#environment)
80 * [Defining Environment Variables](#defining-environment-variables)
81 * [Accessing Environment Variables](#accessing-environment-variables)
82- [Making HTTP Requests](#making-http-requests)
83 * [Shorthand HTTP Requests](#shorthand-http-requests)
84 * [Manual HTTP Requests](#manual-http-requests)
85 + [POST and PUT Requests](#post-and-put-requests)
86 * [Using HTTP middleware](#using-http-middleware)
87 * [HTTP Request Options](#http-request-options)
88 * [HTTP Response Object](#http-response-object)
89- [Dehydration](#dehydration)
90 * [File Dehydration](#file-dehydration)
91- [Stashing Files](#stashing-files)
92- [Logging](#logging)
93 * [Console Logging](#console-logging)
94 * [Viewing Console Logs](#viewing-console-logs)
95 * [Viewing Bundle Logs](#viewing-bundle-logs)
96 * [HTTP Logging](#http-logging)
97 * [Viewing HTTP Logs](#viewing-http-logs)
98- [Error Handling](#error-handling)
99 * [General Errors](#general-errors)
100 * [Halting Execution](#halting-execution)
101 * [Stale Authentication Credentials](#stale-authentication-credentials)
102- [Testing](#testing)
103 * [Writing Unit Tests](#writing-unit-tests)
104 * [Mocking Requests](#mocking-requests)
105 * [Running Unit Tests](#running-unit-tests)
106 * [Testing & Environment Variables](#testing--environment-variables)
107 * [Viewing HTTP Logs in Unit Tests](#viewing-http-logs-in-unit-tests)
108 * [Testing in Your CI](#testing-in-your-ci)
109- [Using `npm` Modules](#using-npm-modules)
110- [Building Native Packages with Docker](#building-native-packages-with-docker)
111- [Using Transpilers](#using-transpilers)
112- [Example Apps](#example-apps)
113- [FAQs](#faqs)
114 * [Why doesn't Zapier support newer versions of Node.js?](#why-doesnt-zapier-support-newer-versions-of-nodejs)
115 * [How do I manually set the Node.js version to run my app with?](#how-do-i-manually-set-the-nodejs-version-to-run-my-app-with)
116 * [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies)
117 * [Does Zapier support XML (SOAP) APIs?](#does-zapier-support-xml-soap-apis)
118 * [Is it possible to iterate over pages in a polling trigger?](#is-it-possible-to-iterate-over-pages-in-a-polling-trigger)
119 * [How do search-powered fields relate to dynamic dropdowns and why are they both required together?](#how-do-search-powered-fields-relate-to-dynamic-dropdowns-and-why-are-they-both-required-together)
120 * [What's the deal with pagination? When is it used and how does it work?](#whats-the-deal-with-pagination-when-is-it-used-and-how-does-it-work)
121 * [How does deduplication work?](#how-does-deduplication-work)
122 * [Why are my triggers complaining if I don't provide an explicit `id` field?](#why-are-my-triggers-complaining-if-i-dont-provide-an-explicit-id-field)
123- [Command Line Tab Completion](#command-line-tab-completion)
124 * [Zsh Completion Script](#zsh-completion-script)
125 * [Bash Completion Script](#bash-completion-script)
126- [The Zapier Platform Packages](#the-zapier-platform-packages)
127 * [Updating](#updating)
128- [Development of the CLI](#development-of-the-cli)
129 * [Commands](#commands)
130 * [Publishing of the CLI (after merging)](#publishing-of-the-cli-after-merging)
131- [Get Help!](#get-help)
132
133<!-- tocstop -->
134
135## Getting Started
136
137> If you're new to Zapier Platform CLI, we strongly recommend you to walk through the [Tutorial](https://zapier.com/developer/start) for a more thorough introduction.
138
139### What is an App?
140
141A CLI App is an implementation of your app's API. You build a Node.js application
142that exports a single object ([JSON Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#appschema)) and upload it to Zapier.
143Zapier introspects that definition to find out what your app is capable of and
144what options to present end users in the Zap Editor.
145
146For those not familiar with Zapier terminology, here is how concepts in the CLI
147map to the end user experience:
148
149 * [Authentication](#authentication), (usually) which lets us know what credentials to ask users
150 for. This is used during the "Connect Accounts" section of the Zap Editor.
151 * [Triggers](#triggerssearchescreates), which read data *from* your API. These have their own section in the Zap Editor.
152 * [Creates](#triggerssearchescreates), which send data *to* your API to create new records. These are listed under "Actions" in the Zap Editor.
153 * [Searches](#triggerssearchescreates), which find specific records *in* your system. These are also listed under "Actions" in the Zap Editor.
154 * [Resources](#resources), which define an object type in your API (say a contact) and the operations available to perform on it. These are automatically extracted into Triggers, Searches, and Creates.
155
156### How does Zapier Platform CLI Work?
157
158Zapier takes the App you upload and sends it over to Amazon Web Service's Lambda.
159We then make calls to execute the operations your App defines as we execute Zaps.
160Your App takes the input data we provide (if any), makes the necessary HTTP calls,
161and returns the relevant data, which gets fed back into Zapier.
162
163### Zapier Platform CLI vs UI
164
165The Zapier Platform includes two ways to build integrations: a CLI to build integrations in your local development environment and deploy them from the command line, and a UI to create integrations with a visual builder from your browser. Both use the same Zapier platform, so pick the one that fits your team's needs best, as the difference is in how you develop the integration.
166
167Zapier Platform CLI is designed to be used by development teams who collaborate with version control and CI, and can be used to build more advanced integrations with custom coding for every part of your API calls and response parsing.
168
169[Zapier Platform UI](https://zapier.com/app/developer/) is designed to quickly spin up new integrations, and collaborate on them with teams that include non-developers. It's the quickest way to start using the Zapier platform—and you can manage your CLI apps' core details from its online UI as well. Coming soon, you will be able to convert Zapier Platform UI integrations to CLI to start development in your browser then finish out the core features in your local development environment.
170
171_→ Learn more in our [Zapier Platform UI vs CLI](https://zapier.com/developer/documentation/v2/ui-vs-cli/) post._
172
173### Requirements
174
175All Zapier CLI apps are run using Node.js `v8.10.0`.
176
177You can develop using any version of Node you'd like, but your eventual code must be compatible with `v8.10.0`. If you're using features not yet available in `v8.10.0`, you can transpile your code to a compatible format with [Babel](https://babeljs.io/) (or similar).
178
179To ensure stability for our users, we strongly encourage you run tests on `v8.10.0` sometime before your code reaches users. This can be done multiple ways.
180
181Firstly, by using a CI tool (like [Travis CI](https://travis-ci.org/) or [Circle CI](https://circleci.com/), which are free for open source projects). We provide a sample [.travis.yml](https://github.com/zapier/zapier-platform-example-app-minimal/blob/master/.travis.yml) file in our template apps to get you started.
182
183Alternatively, you can change your local node version with tools such as [nvm](https://github.com/creationix/nvm#installation) or [n](https://github.com/tj/n#installation).
184Then you can either swap to that version with `nvm use v8.10.0`, or do `nvm exec v8.10.0 zapier test` so you can run tests without having to switch versions while developing.
185
186
187### Quick Setup Guide
188
189First up is installing the CLI and setting up your auth to create a working "Zapier Example" application. It will be private to you and visible in your live [Zap Editor](https://zapier.com/app/editor).
190
191```bash
192# install the CLI globally
193npm install -g zapier-platform-cli
194
195# setup auth to Zapier's platform with a deploy key
196zapier login
197```
198
199Your Zapier CLI should be installed and ready to go at this point. Next up, we'll create our first app!
200
201```bash
202# create a directory with the minimum required files
203zapier init example-app
204
205# move into the new directory
206cd example-app
207
208# install all the libraries needed for your app
209npm install
210```
211
212> Note: there are plenty of templates & example apps to choose from! [View all Example Apps here.](#example-apps).
213
214You should now have a working local app. You can run several local commands to try it out.
215
216```bash
217# run the local tests
218# the same as npm test, but adds some extra things to the environment
219zapier test
220```
221
222Next, you'll probably want to upload app to Zapier itself so you can start testing live.
223
224```bash
225# push your app to Zapier
226zapier push
227```
228
229> Go check out our [full CLI reference documentation](http://zapier.github.io/zapier-platform-cli/cli.html) to see all the other commands!
230
231
232### Tutorial
233
234For a full tutorial, head over to our [Tutorial](https://zapier.com/developer/start) for a comprehensive walkthrough for creating your first app. If this isn't your first rodeo, read on!
235
236## Creating a Local App
237
238> Tip: check the [Quick Setup](#quick-setup-guide) if this is your first time using the platform!
239
240Creating an App can be done entirely locally and they are fairly simple Node.js apps using the standard Node environment and should be completely testable. However, a local app stays local until you `zapier register`.
241
242```bash
243# make your folder
244mkdir zapier-example
245cd zapier-example
246
247# create the needed files from a template
248zapier init . --template=trigger
249
250# install all the libraries needed for your app
251npm install
252```
253
254If you'd like to manage your **local App**, use these commands:
255
256* `zapier init . --template=resource` - initialize/start a local app project ([see templates here](https://github.com/zapier/zapier-platform-cli/wiki/Example-Apps))
257* `zapier convert 1234 .` - initialize/start from an existing app (alpha)
258* `zapier scaffold resource Contact` - auto-injects a new resource, trigger, etc.
259* `zapier test` - run the same tests as `npm test`
260* `zapier validate` - ensure your app is valid
261* `zapier describe` - print some helpful information about your app
262
263### Local Project Structure
264
265In your app's folder, you should see this general recommended structure. The `index.js` is Zapier's entry point to your app. Zapier expects you to export an `App` definition there.
266
267```plain
268$ tree .
269.
270├── README.md
271├── index.js
272├── package.json
273├── triggers
274│   └── contact-by-tag.js
275├── resources
276│   └── Contact.js
277├── test
278│   ├── basic.js
279│   ├── triggers.js
280│   └── resources.js
281├── build
282│   └── build.zip
283└── node_modules
284 ├── ...
285 └── ...
286```
287
288### Local App Definition
289
290The core definition of your `App` will look something like this, and is what your `index.js` should provide as the _only_ export:
291
292```js
293const App = {
294 // both version strings are required
295 version: require('./package.json').version,
296 platformVersion: require('zapier-platform-core').version,
297
298 // see "Authentication" section below
299 authentication: {},
300
301 // see "Dehydration" section below
302 hydrators: {},
303
304 // see "Making HTTP Requests" section below
305 requestTemplate: {},
306 beforeRequest: [],
307 afterResponse: [],
308
309 // See "Resources" section below
310 resources: {},
311
312 // See "Triggers/Searches/Creates" section below
313 triggers: {},
314 searches: {},
315 creates: {}
316};
317
318module.exports = App;
319
320```
321
322> Tip: you can use higher order functions to create any part of your App definition!
323
324
325## Registering an App
326
327Registering your App with Zapier is a necessary first step which only enables basic administrative functions. It should happen before `zapier push` which is to used to actually expose an App Version in the Zapier interface and editor.
328
329```bash
330# register your app
331zapier register "Zapier Example"
332
333# list your apps
334zapier apps
335```
336
337> Note: this doesn't put your app in the editor - see the docs on pushing an App Version to do that!
338
339If you'd like to manage your **App**, use these commands:
340
341* `zapier apps` - list the apps in Zapier you can administer
342* `zapier register "Name"` - creates a new app in Zapier
343* `zapier link` - lists and links a selected app in Zapier to your current folder
344* `zapier history` - print the history of your app
345* `zapier collaborate [user@example.com]` - add admins to your app who can push
346* `zapier invite [user@example.com] [1.0.0]` - add users to try your app version 1.0.0 before promotion
347
348
349## Deploying an App Version
350
351An App Version is related to a specific App but is an "immutable" implementation of your app. This makes it easy to run multiple versions for multiple users concurrently. The App Version is pulled from the version within the `package.json`. To create a new App Version, update the version number in that file. By default, **every App Version is private** but you can `zapier promote` it to production for use by over 1 million Zapier users.
352
353```bash
354# push your app version to Zapier
355zapier push
356
357# list your versions
358zapier versions
359```
360
361If you'd like to manage your **Version**, use these commands:
362
363* `zapier versions` - list the versions for the current directory's app
364* `zapier push` - push the current version of current directory's app & version (read from `package.json`)
365* `zapier promote [1.0.0]` - mark a version as the "production" version
366* `zapier migrate [1.0.0] [1.0.1] [100%]` - move users between versions, regardless of deployment status
367* `zapier deprecate [1.0.0] [YYYY-MM-DD]` - mark a version as deprecated, but let users continue to use it (we'll email them)
368* `zapier env 1.0.0 [KEY] [value]` - set an environment variable to some value
369* `zapier delete version [1.0.0]` - delete a version entirely. This is mostly for clearing out old test apps you used personally. It will fail if there are any users. You probably want `deprecate` instead.
370
371> Note: To see the changes that were just pushed reflected in the browser, you have to manually refresh the browser each time you push.
372
373
374### Private App Version (default)
375
376A simple `zapier push` will only create the App Version in your editor. No one else using Zapier can see it or use it.
377
378
379### Sharing an App Version
380
381This is how you would share your app with friends, co-workers or clients. This is perfect for quality assurance, testing with active users or just sharing any app you like.
382
383```bash
384# sends an email this user to let them view the app version 1.0.0 in the UI privately
385zapier invite user@example.com 1.0.0
386
387# sends an email this user to let them admin the app (make changes just like you)
388zapier collaborate user@example.com
389```
390
391You can also invite anyone on the internet to your app by observing the URL at the bottom of `zapier invite`, it should look something like `https://zapier.com/platform/public-invite/1/222dcd03aed943a8676dc80e2427a40d/`. You can put this in your help docs, post it to Twitter, add it to your email campaign, etc. Note this will invite users to every app version.
392
393
394### Promoting an App Version
395
396Promotion is how you would share your app with every one of the 1 million+ Zapier users. If this is your first time promoting - you may have to wait for the Zapier team to review and approve your app.
397
398If this isn't the first time you've promoted your app - you might have users on older versions. You can `zapier migrate` to either move users over (which can be dangerous if you have breaking changes). Or, you can `zapier deprecate` to give users some time to move over themselves.
399
400```bash
401# promote your app version to all Zapier users
402zapier promote 1.0.1
403
404# OPTIONAL - migrate your users between one app version to another
405zapier migrate 1.0.0 1.0.1
406
407# OR - mark the old version as deprecated
408zapier deprecate 1.0.0 2017-01-01
409```
410
411## Converting an Existing App
412
413If you have an existing Zapier [legacy Web Builder app](https://zapier.com/developer/builder/), you can use it as a template to kickstart your local application.
414
415```bash
416# Convert an existing Web Builder app to a CLI app in the my-app directory
417# App ID 1234 is from URL https://zapier.com/developer/builder/app/1234/development
418zapier convert 1234 my-app
419```
420
421Your CLI app will be created and you can continue working on it.
422
423> Since v3.3.0, `zapier convert` has been improved a lot. But this is still in an alpha state - you'll likely have to edit the code to make it work.
424
425> Note - there is no way to convert a CLI app to a Web Builder app and we do not plan on implementing this.
426
427Coming soon, you will also be able to convert new integrations built in Zapier Platform UI to CLI.
428
429## Authentication
430
431Most applications require some sort of authentication - and Zapier provides a handful of methods for helping your users authenticate with your application. Zapier will provide some of the core behaviors, but you'll likely need to handle the rest.
432
433> Hint: You can access the data tied to your authentication via the `bundle.authData` property in any method called in your app. Exceptions exist in OAuth and Session auth. Please see them below.
434
435### Basic
436
437Useful if your app requires two pieces of information to authentication: `username` and `password` which only the end user can provide. By default, Zapier will do the standard Basic authentication base64 header encoding for you (via an automatically registered middleware).
438
439> Example App: check out https://github.com/zapier/zapier-platform-example-app-basic-auth for a working example app for basic auth.
440
441> Note: if you do the common API Key pattern like `Authorization: Basic APIKEYHERE:x` you should look at the "Custom" authentication method instead.
442
443```js
444const authentication = {
445 type: 'basic',
446 // "test" could also be a function
447 test: {
448 url: 'https://example.com/api/accounts/me.json'
449 },
450 connectionLabel: '{{bundle.authData.username}}' // Can also be a function, check digest auth below for an example
451 // you can provide additional fields, but we'll provide `username`/`password` automatically
452};
453
454const App = {
455 // ...
456 authentication: authentication
457 // ...
458};
459
460```
461
462### Digest
463
464*New in v7.4.0.*
465
466The setup and user experience of Digest Auth is identical to Basic Auth. Users will provide Zapier their username and password and Zapier will handle all the nonce and quality of protection details automatically.
467
468> Example App: check out https://github.com/zapier/zapier-platform-example-app-digest-auth for a working example app for digest auth.
469
470> Limitation: Currently, MD5-sess and SHA are not implemented. Only the MD5 algorithm is supported. In addition, server nonces are not reused. That means for every `z.request` call, Zapier will sends an additional request beforehand to get the server nonce.
471
472```js
473const authentication = {
474 type: 'digest',
475 // "test" could also be a function
476 test: {
477 url: 'https://example.com/api/accounts/me.json'
478 },
479 connectionLabel: '{{bundle.authData.username}}' // Can also be a function, check digest auth below for an example
480 // you can provide additional fields, but we'll provide `username`/`password` automatically
481};
482
483const App = {
484 // ...
485 authentication: authentication
486 // ...
487};
488
489```
490
491### Custom
492
493This is what most "API Key" driven apps should default to using. You'll likely provide some custom `beforeRequest` middleware or a `requestTemplate` to complete the authentication by adding/computing needed headers.
494
495> Example App: check out https://github.com/zapier/zapier-platform-example-app-custom-auth for a working example app for custom auth.
496
497```js
498const authentication = {
499 type: 'custom',
500 // "test" could also be a function
501 test: {
502 url:
503 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json'
504 },
505 fields: [
506 {
507 key: 'subdomain',
508 type: 'string',
509 required: true,
510 helpText: 'Found in your browsers address bar after logging in.'
511 },
512 {
513 key: 'api_key',
514 type: 'string',
515 required: true,
516 helpText: 'Found on your settings page.'
517 }
518 ]
519};
520
521const addApiKeyToHeader = (request, z, bundle) => {
522 request.headers['X-Subdomain'] = bundle.authData.subdomain;
523 const basicHash = Buffer(`${bundle.authData.api_key}:x`).toString('base64');
524 request.headers.Authorization = `Basic ${basicHash}`;
525 return request;
526};
527
528const App = {
529 // ...
530 authentication: authentication,
531 beforeRequest: [addApiKeyToHeader]
532 // ...
533};
534
535```
536
537### Session
538
539Probably the most "powerful" mechanism for authentication - it gives you the ability to exchange some user provided data for some authentication data (IE: username & password for a session key).
540
541> Example App: check out https://github.com/zapier/zapier-platform-example-app-session-auth for a working example app for session auth.
542
543```js
544const getSessionKey = (z, bundle) => {
545 const promise = z.request({
546 method: 'POST',
547 url: 'https://example.com/api/accounts/login.json',
548 body: {
549 username: bundle.authData.username,
550 password: bundle.authData.password
551 }
552 });
553
554 return promise.then(response => {
555 if (response.status === 401) {
556 throw new Error('The username/password you supplied is invalid');
557 }
558 return {
559 sessionKey: z.JSON.parse(response.content).sessionKey
560 };
561 });
562};
563
564const authentication = {
565 type: 'session',
566 // "test" could also be a function
567 test: {
568 url: 'https://example.com/api/accounts/me.json'
569 },
570 fields: [
571 {
572 key: 'username',
573 type: 'string',
574 required: true,
575 helpText: 'Your login username.'
576 },
577 {
578 key: 'password',
579 type: 'string',
580 required: true,
581 helpText: 'Your login password.'
582 }
583 // For Session Auth we store `sessionKey` automatically in `bundle.authData`
584 // for future use. If you need to save/use something that the user shouldn't
585 // need to type/choose, add a "computed" field, like:
586 // {key: 'something': type: 'string', required: false, computed: true}
587 // And remember to return it in sessionConfig.perform
588 ],
589 sessionConfig: {
590 perform: getSessionKey
591 }
592};
593
594const includeSessionKeyHeader = (request, z, bundle) => {
595 if (bundle.authData.sessionKey) {
596 request.headers = request.headers || {};
597 request.headers['X-Session-Key'] = bundle.authData.sessionKey;
598 }
599 return request;
600};
601
602const sessionRefreshIf401 = (response, z, bundle) => {
603 if (bundle.authData.sessionKey) {
604 if (response.status === 401) {
605 throw new z.errors.RefreshAuthError(); // ask for a refresh & retry
606 }
607 }
608 return response;
609};
610
611const App = {
612 // ...
613 authentication: authentication,
614 beforeRequest: [includeSessionKeyHeader],
615 afterResponse: [sessionRefreshIf401]
616 // ...
617};
618
619```
620
621> Note - For Session auth, `authentication.sessionConfig.perform` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty the first time the Zap runs.
622
623### OAuth1
624
625*New in v7.5.0.*
626
627Zapier's OAuth1 implementation matches [Twitter's](https://developer.twitter.com/en/docs/basics/authentication/overview) and [Trello's](https://developers.trello.com/page/authorization) implementation of the 3-legged OAuth flow.
628
629> Example Apps: check out [oauth1-trello](https://github.com/zapier/zapier-platform-example-app-oauth1-trello), [oauth1-tumblr](https://github.com/zapier/zapier-platform-example-app-oauth1-tumblr), and [oauth1-twitter](https://github.com/zapier/zapier-platform-example-app-oauth1-twitter) for working example apps with OAuth1.
630
631The flow works like this:
632
633 1. Zapier makes a call to your API requesting a "request token" (also known as "temporary credentials")
634 2. Zapier sends the user to the authorization URL, defined by your app, along with the request token
635 3. Once authorized, your website sends the user to the `redirect_uri` Zapier provided. Use `zapier describe` command to find out what it is: ![](https://zappy.zapier.com/117ECB35-5CCA-4C98-B74A-35F1AD9A3337.png)
636 4. Zapier makes a call on our backend to your API to exchange the request token for an "access token" (also known as "long-lived credentials")
637 5. Zapier remembers the access token and makes calls on behalf of the user
638
639You are required to define:
640
641 * `getRequestToken`: The API call to fetch the request token
642 * `authorizeUrl`: The authorization URL
643 * `getAccessToken`: The API call to fetch the access token
644
645You'll also likely need to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables. These are the consumer key and secret in OAuth1 terminology.
646
647```bash
648# setting the environment variables on Zapier.com
649$ zapier env 1.0.0 CLIENT_ID 1234
650$ zapier env 1.0.0 CLIENT_SECRET abcd
651
652# and when running tests locally, don't forget to define them in .env or in the command!
653$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
654```
655
656Your auth definition would look something like this:
657
658```js
659const _ = require('lodash');
660
661const authentication = {
662 type: 'oauth1',
663 oauth1Config: {
664 getRequestToken: {
665 url: 'https://{{bundle.inputData.subdomain}}.example.com/request-token',
666 method: 'POST',
667 auth: {
668 oauth_consumer_key: '{{process.env.CLIENT_ID}}',
669 oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
670
671 // 'HMAC-SHA1' is used by default if not specified.
672 // 'HMAC-SHA256', 'RSA-SHA1', 'PLAINTEXT' are also supported.
673 oauth_signature_method: 'HMAC-SHA1',
674 oauth_callback: '{{bundle.inputData.redirect_uri}}'
675 }
676 },
677 authorizeUrl: {
678 url: 'https://{{bundle.inputData.subdomain}}.example.com/authorize',
679 params: {
680 oauth_token: '{{bundle.inputData.oauth_token}}'
681 }
682 },
683 getAccessToken: {
684 url: 'https://{{bundle.inputData.subdomain}}.example.com/access-token',
685 method: 'POST',
686 auth: {
687 oauth_consumer_key: '{{process.env.CLIENT_ID}}',
688 oauth_consumer_secret: '{{process.env.CLIENT_SECRET}}',
689 oauth_token: '{{bundle.inputData.oauth_token}}',
690 oauth_token_secret: '{{bundle.inputData.oauth_token_secret}}',
691 oauth_verifier: '{{bundle.inputData.oauth_verifier}}'
692 }
693 }
694 },
695 test: {
696 url: 'https://{{bundle.authData.subdomain}}.example.com/me'
697 },
698 // If you need any fields upfront, put them here
699 fields: [
700 { key: 'subdomain', type: 'string', required: true, default: 'app' }
701 // For OAuth1 we store `oauth_token` and `oauth_token_secret` automatically
702 // in `bundle.authData` for future use. If you need to save/use something
703 // that the user shouldn't need to type/choose, add a "computed" field, like:
704 // {key: 'user_id': type: 'string', required: false, computed: true}
705 // And remember to return it in oauth1Config.getAccessToken
706 ]
707};
708
709// A middleware that is run before z.request() actually makes the request. Here we're
710// adding necessary OAuth1 parameters to `auth` property of the request object.
711const includeAccessToken = (req, z, bundle) => {
712 if (
713 bundle.authData &&
714 bundle.authData.oauth_token &&
715 bundle.authData.oauth_token_secret
716 ) {
717 // Just put your OAuth1 credentials in req.auth, Zapier will sign the request for
718 // you.
719 req.auth = req.auth || {};
720 _.defaults(req.auth, {
721 oauth_consumer_key: process.env.CLIENT_ID,
722 oauth_consumer_secret: process.env.CLIENT_SECRET,
723 oauth_token: bundle.authData.oauth_token,
724 oauth_token_secret: bundle.authData.oauth_token_secret
725 });
726 }
727 return req;
728};
729
730const App = {
731 // ...
732 authentication: authentication,
733 beforeRequest: [includeAccessToken]
734 // ...
735};
736
737module.exports = App;
738
739```
740
741> Note - For OAuth1, `authentication.oauth1Config.getRequestToken`, `authentication.oauth1Config.authorizeUrl`, and `authentication.oauth1Config.getAccessToken` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty when the user hasn't connected their account on your service to Zapier. Also note that `authentication.oauth1Config.getAccessToken` has access to the users return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example from the query string).
742
743### OAuth2
744
745Zapier's OAuth2 implementation is based on the `authorization_code` flow, similar to [GitHub](http://developer.github.com/v3/oauth/) and [Facebook](https://developers.facebook.com/docs/authentication/server-side/).
746
747> Example App: check out https://github.com/zapier/zapier-platform-example-app-oauth2 for a working example app for OAuth2.
748
749It looks like this:
750
751 1. Zapier sends the user to the authorization URL defined by your app
752 2. Once authorized, your website sends the user to the `redirect_uri` Zapier provided. Use `zapier describe` command to find out what it is: ![](https://zappy.zapier.com/83E12494-0A03-4DB4-AA46-5A2AF6A9ECCC.png)
753 3. Zapier makes a call on our backend to your API to exchange the `code` for an `access_token`
754 4. Zapier remembers the `access_token` and makes calls on behalf of the user
755 5. (Optionally) Zapier can refresh the token if it expires
756
757You are required to define the authorization URL and the API call to fetch the access token. You'll also likely want to set your `CLIENT_ID` and `CLIENT_SECRET` as environment variables:
758
759```bash
760# setting the environment variables on Zapier.com
761$ zapier env 1.0.0 CLIENT_ID 1234
762$ zapier env 1.0.0 CLIENT_SECRET abcd
763
764# and when running tests locally, don't forget to define them in .env or in the command!
765$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
766```
767
768Your auth definition would look something like this:
769
770```js
771const authentication = {
772 type: 'oauth2',
773 test: {
774 url:
775 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json'
776 },
777 // you can provide additional fields for inclusion in authData
778 oauth2Config: {
779 // "authorizeUrl" could also be a function returning a string url
780 authorizeUrl: {
781 method: 'GET',
782 url:
783 'https://{{bundle.inputData.subdomain}}.example.com/api/oauth2/authorize',
784 params: {
785 client_id: '{{process.env.CLIENT_ID}}',
786 state: '{{bundle.inputData.state}}',
787 redirect_uri: '{{bundle.inputData.redirect_uri}}',
788 response_type: 'code'
789 }
790 },
791 // Zapier expects a response providing {access_token: 'abcd'}
792 // "getAccessToken" could also be a function returning an object
793 getAccessToken: {
794 method: 'POST',
795 url:
796 'https://{{bundle.inputData.subdomain}}.example.com/api/v2/oauth2/token',
797 body: {
798 code: '{{bundle.inputData.code}}',
799 client_id: '{{process.env.CLIENT_ID}}',
800 client_secret: '{{process.env.CLIENT_SECRET}}',
801 redirect_uri: '{{bundle.inputData.redirect_uri}}',
802 grant_type: 'authorization_code'
803 },
804 headers: {
805 'Content-Type': 'application/x-www-form-urlencoded'
806 }
807 },
808 scope: 'read,write'
809 },
810 // If you need any fields upfront, put them here
811 fields: [
812 { key: 'subdomain', type: 'string', required: true, default: 'app' }
813 // For OAuth2 we store `access_token` and `refresh_token` automatically
814 // in `bundle.authData` for future use. If you need to save/use something
815 // that the user shouldn't need to type/choose, add a "computed" field, like:
816 // {key: 'user_id': type: 'string', required: false, computed: true}
817 // And remember to return it in oauth2Config.getAccessToken/refreshAccessToken
818 ]
819};
820
821const addBearerHeader = (request, z, bundle) => {
822 if (bundle.authData && bundle.authData.access_token) {
823 request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
824 }
825 return request;
826};
827
828const App = {
829 // ...
830 authentication: authentication,
831 beforeRequest: [addBearerHeader]
832 // ...
833};
834
835module.exports = App;
836
837```
838
839> Note - For OAuth2, `authentication.oauth2Config.authorizeUrl`, `authentication.oauth2Config.getAccessToken`, and `authentication.oauth2Config.refreshAccessToken` will have the provided fields in `bundle.inputData` instead of `bundle.authData` because `bundle.authData` will only have "previously existing" values, which will be empty when the user hasn't connected their account on your service to Zapier. Also note that `authentication.oauth2Config.getAccessToken` has access to the users return values in `rawRequest` and `cleanedRequest` should you need to extract other values (for example from the query string).
840
841
842## Resources
843
844A `resource` is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a `/recipes`
845endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
846read, and search operations on that resource.
847
848```js
849const Recipe = {
850 // `key` is the unique identifier the Zapier backend references
851 key: 'recipe',
852 // `noun` is the user-friendly name displayed in the Zapier UI
853 noun: 'Recipe',
854 // `list` and `create` are just a couple of the methods you can define
855 list: {
856 //...
857 },
858 create: {
859 //...
860 }
861};
862
863```
864
865The quickest way to create a resource is with the `zapier scaffold` command:
866
867```bash
868zapier scaffold resource "Recipe"
869```
870
871This will generate the resource file and add the necessary statements to the `index.js` file to import it.
872
873
874### Resource Definition
875
876A resource has a few basic properties. The first is the `key`, which allows Zapier to identify the resource on our backend.
877The second is the `noun`, the user-friendly name of the resource that is presented to users throughout the Zapier UI.
878
879> Example App: check out https://github.com/zapier/zapier-platform-example-app-resource for a working example app using resources.
880
881After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource.
882The complete list of available methods can be found in the [Resource Schema Docs](https://zapier.github.io/zapier-platform-schema/build/schema.html#resourceschema).
883For now, let's focus on two:
884
885 * `list` - Tells Zapier how to fetch a set of this resource. This becomes a Trigger in the Zapier Editor.
886 * `create` - Tells Zapier how to create a new instance of the resource. This becomes an Action in the Zapier Editor.
887
888Here is a complete example of what the list method might look like
889
890```js
891const listRecipesRequest = {
892 url: 'http://example.com/recipes'
893};
894
895const Recipe = {
896 key: 'recipe',
897 //...
898 list: {
899 display: {
900 label: 'New Recipe',
901 description: 'Triggers when a new recipe is added.'
902 },
903 operation: {
904 perform: listRecipesRequest
905 }
906 }
907};
908
909```
910
911The method is made up of two properties, a `display` and an `operation`. The `display` property ([schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#basicdisplayschema)) holds the info needed to present the method as an available Trigger in the Zapier Editor. The `operation` ([schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#resourceschema)) provides the implementation to make the API call.
912
913Adding a create method looks very similar.
914
915```js
916const createRecipeRequest = {
917 url: 'http://example.com/recipes',
918 method: 'POST',
919 body: {
920 name: 'Baked Falafel',
921 style: 'mediterranean'
922 }
923};
924
925const Recipe = {
926 key: 'recipe',
927 //...
928 list: {
929 //...
930 },
931 create: {
932 display: {
933 label: 'Add Recipe',
934 description: 'Adds a new recipe to our cookbook.'
935 },
936 operation: {
937 perform: createRecipeRequest
938 }
939 }
940};
941
942```
943
944Every method you define on a `resource` Zapier converts to the appropriate Trigger, Create, or Search. Our examples
945above would result in an app with a New Recipe Trigger and an Add Recipe Create.
946
947Note the keys for the Trigger, Create, Search, and Search or Create are automatically generated (in case you want to use them in a dynamic dropdown), like: `{resourceName}List`, `{resourceName}Create`, `{resourceName}Search`, and `{resourceName}SearchOrCreate`; in the examples above, `{resourceName}` would be `recipe`.
948
949
950## Triggers/Searches/Creates
951
952Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read
953data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create
954new records in your system (add a recipe to the catalog).
955
956The definition for each of these follows the same structure. Here is an example of a trigger:
957
958```js
959const recipeListRequest = {
960 url: 'http://example.com/recipes'
961};
962
963const App = {
964 //...
965 triggers: {
966 new_recipe: {
967 key: 'new_recipe', // uniquely identifies the trigger
968 noun: 'Recipe', // user-friendly word that is used to refer to the resource
969 // `display` controls the presentation in the Zapier Editor
970 display: {
971 label: 'New Recipe',
972 description: 'Triggers when a new recipe is added.'
973 },
974 // `operation` implements the API call used to fetch the data
975 operation: {
976 perform: recipeListRequest
977 }
978 },
979 another_trigger: {
980 // Another trigger definition...
981 }
982 }
983};
984
985```
986
987You can find more details on the definition for each by looking at the [Trigger Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#triggerschema),
988[Search Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#searchschema), and [Create Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#createschema).
989
990> Example App: check out https://github.com/zapier/zapier-platform-example-app-trigger for a working example app using triggers.
991
992> Example App: check out https://github.com/zapier/zapier-platform-example-app-rest-hooks for a working example app using REST hook triggers.
993
994> Example App: check out https://github.com/zapier/zapier-platform-example-app-search for a working example app using searches.
995
996> Example App: check out https://github.com/zapier/zapier-platform-example-app-create for a working example app using creates.
997
998### Return Types
999
1000Each of the 3 types of function expects a certain type of object. As of core `v1.0.11`, there are automated checks to let you know when you're trying to pass the wrong type back. There's more info in each relevant `post_X` section of the [v2 docs](https://zapier.com/developer/documentation/v2/scripting/#available-methods). For reference, each expects:
1001
1002| Method | Return Type | Notes |
1003| --- | --- | --- |
1004| Trigger | Array | 0 or more objects that will be passed to the [deduper](https://zapier.com/developer/documentation/v2/deduplication/) |
1005| Search | Array | 0 or more objects. If len > 0, put the best match first |
1006| Action | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |
1007
1008## Input Fields
1009
1010On each trigger, search, or create in the `operation` directive - you can provide an array of objects as fields under the `inputFields`. Input Fields are what your users would see in the main Zapier user interface. For example, you might have a "Create Contact" action with fields like "First name", "Last name", "Email", etc. These fields will be able to accept input from previous steps in a Zap, for example:
1011
1012![gif of setting up an action field in Zap Editor](https://cdn.zapier.com/storage/photos/6bd938f7cad7e34c75ba1c1d3be75ac5.gif)
1013
1014You can find more details about setting action fields from a user perspective in [our help documentation](https://zapier.com/help/creating-zap/#set-up-action-template).
1015
1016Those fields have various options you can provide, here is a succinct example:
1017
1018```js
1019const App = {
1020 //...
1021 creates: {
1022 create_recipe: {
1023 //...
1024 operation: {
1025 // an array of objects is the simplest way
1026 inputFields: [
1027 {
1028 key: 'title',
1029 required: true,
1030 label: 'Title of Recipe',
1031 helpText: 'Name your recipe!'
1032 },
1033 {
1034 key: 'style',
1035 required: true,
1036 choices: { mexican: 'Mexican', italian: 'Italian' }
1037 }
1038 ],
1039 perform: () => {}
1040 }
1041 }
1042 }
1043};
1044
1045```
1046
1047You can find more details on the different field schema options at [our Field Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#fieldschema).
1048
1049### Custom/Dynamic Fields
1050
1051In some cases, it might be necessary to provide fields that are dynamically generated - especially for custom fields. This is a common pattern for CRMs, form software, databases and more. Basically - you can provide a function instead of a field and we'll evaluate that function - merging the dynamic fields with the static fields.
1052
1053> You should see `bundle.inputData` partially filled in as users provide data - even in field retrieval. This allows you to build hierarchical relationships into fields (EG: only show issues from the previously selected project).
1054
1055> A function that returns a list of dynamic fields cannot include additional functions in that list to call for dynamic fields.
1056
1057```js
1058const recipeFields = (z, bundle) => {
1059 const response = z.request('http://example.com/api/v2/fields.json');
1060 // json is is [{"key":"field_1"},{"key":"field_2"}]
1061 return response.then(res => res.json);
1062};
1063
1064const App = {
1065 //...
1066 creates: {
1067 create_recipe: {
1068 //...
1069 operation: {
1070 // an array of objects is the simplest way
1071 inputFields: [
1072 {
1073 key: 'title',
1074 required: true,
1075 label: 'Title of Recipe',
1076 helpText: 'Name your recipe!'
1077 },
1078 {
1079 key: 'style',
1080 required: true,
1081 choices: { mexican: 'Mexican', italian: 'Italian' }
1082 },
1083 recipeFields // provide a function inline - we'll merge the results!
1084 ],
1085 perform: () => {}
1086 }
1087 }
1088 }
1089};
1090
1091```
1092
1093Additionally, if there is a field that affects the generation of dynamic fields, you can set the `altersDynamicFields: true` property. This informs the Zapier UI that whenever the value of that field changes, fields need to be recomputed. An example could be a static dropdown of "dessert type" that will change whether the function that generates dynamic fields includes a field "with sprinkles." If your field affects others, this is an important property to set.
1094
1095```js
1096module.exports = {
1097 key: 'dessert',
1098 noun: 'Dessert',
1099 display: {
1100 label: 'Order Dessert',
1101 description: 'Orders a dessert.'
1102 },
1103 operation: {
1104 inputFields: [
1105 {
1106 key: 'type',
1107 required: true,
1108 choices: { 1: 'cake', 2: 'ice cream', 3: 'cookie' },
1109 altersDynamicFields: true
1110 },
1111 function(z, bundle) {
1112 if (bundle.inputData.type === '2') {
1113 return [{ key: 'with_sprinkles', type: 'boolean' }];
1114 }
1115 return [];
1116 }
1117 ],
1118 perform: function(z, bundle) {
1119 /* ... */
1120 }
1121 }
1122};
1123
1124```
1125
1126> Only dropdowns support `altersDynamicFields`.
1127
1128### Dynamic Dropdowns
1129
1130Sometimes, API endpoints require clients to specify a parent object in order to create or access the child resources. For instance, specifying a spreadsheet id in order to retrieve its worksheets. Since people don't speak in auto-incremented ID's, it is necessary that Zapier offer a simple way to select that parent using human readable handles.
1131
1132Our solution is to present users a dropdown that is populated by making a live API call to fetch a list of parent objects. We call these special dropdowns "dynamic dropdowns."
1133
1134To define one you include the `dynamic` property on the `inputFields` object. The value for the property is a dot-separated _string_ concatenation.
1135
1136```js
1137//...
1138issue: {
1139 key: 'issue',
1140 //...
1141 create: {
1142 //...
1143 operation: {
1144 inputFields: [
1145 {
1146 key: 'project_id',
1147 required: true,
1148 label: 'This is a dynamic dropdown',
1149 dynamic: 'project.id.name'
1150 }, // will call the trigger with a key of project
1151 {
1152 key: 'title',
1153 required: true,
1154 label: 'Title',
1155 helpText: 'What is the name of the issue?'
1156 }
1157 ]
1158 }
1159 }
1160}
1161
1162```
1163
1164The dot-separated string concatenation follows this pattern:
1165
1166- The key of the trigger you want to use to power the dropdown. _required_
1167- The value to be made available in bundle.inputData. _required_
1168- The human friendly value to be shown on the left of the dropdown in bold. _optional_
1169
1170In the above code example the dynamic property makes reference to a trigger with a key of project. Assuming the project trigger returns an array of objects and each object contains an id and name key, i.e.
1171
1172```js
1173[
1174 { id: '1', name: 'First Option', dateCreated: '01/01/2000' },
1175 { id: '2', name: 'Second Option', dateCreated: '01/01/2000' },
1176 { id: '3', name: 'Third Option', dateCreated: '01/01/2000' },
1177 { id: '4', name: 'Fourth Option', dateCreated: '01/01/2000' }
1178];
1179
1180```
1181
1182The dynamic dropdown would look something like this.
1183![screenshot of dynamic dropdown in Zap Editor](https://cdn.zapier.com/storage/photos/dd31fa761e0cf9d0abc9b50438f95210.png)
1184
1185In the first code example the dynamic dropdown is powered by a trigger. You can also use a resource to power a dynamic dropdown. To do this combine the resource key and the resource method using camel case.
1186
1187```js
1188const App = {
1189 //...
1190 resources: {
1191 project: {
1192 key: 'project',
1193 //...
1194 list: {
1195 //...
1196 operation: {
1197 perform: () => {
1198 return [{ id: 123, name: 'Project 1' }];
1199 } // called for project_id dropdown
1200 }
1201 }
1202 },
1203 issue: {
1204 key: 'issue',
1205 //...
1206 create: {
1207 //...
1208 operation: {
1209 inputFields: [
1210 {
1211 key: 'project_id',
1212 required: true,
1213 label: 'Project',
1214 dynamic: 'projectList.id.name'
1215 }, // calls project.list
1216 {
1217 key: 'title',
1218 required: true,
1219 label: 'Title',
1220 helpText: 'What is the name of the issue?'
1221 }
1222 ]
1223 }
1224 }
1225 }
1226 }
1227};
1228
1229```
1230
1231In some cases you will need to power a dynamic dropdown but do not want to make the Trigger available to the end user. Here it is best practice to create the trigger and set `hidden: true` on it's display object.
1232
1233```js
1234const App = {
1235 //...
1236 triggers: {
1237 new_project: {
1238 key: 'project',
1239 noun: 'Project',
1240 // `display` controls the presentation in the Zapier Editor
1241 display: {
1242 label: 'New Project',
1243 description: 'Triggers when a new project is added.',
1244 hidden: true
1245 },
1246 operation: {
1247 perform: projectListRequest
1248 }
1249 },
1250 another_trigger: {
1251 // Another trigger definition...
1252 }
1253 }
1254};
1255
1256```
1257
1258You can have multiple dynamic dropdowns in a single Trigger or Action. And a dynamic dropdown can depend on the value chosen in another dynamic dropdown when making it's API call. Such as a Spreadsheet and Worksheet dynamic dropdown in a trigger or action. This means you must make sure that the key of the first dynamic dropdown is the same as referenced in the trigger powering the second.
1259
1260Let's say you have a Worksheet trigger with a `perform` method similar to this.
1261
1262```js
1263perform: () => {
1264 return z
1265 .request('http://example.com/api/v2/projects.json', {
1266 params: {
1267 spreadsheet_id: bundle.inputData.spreadsheet_id
1268 }
1269 })
1270 .then(response => z.JSON.parse(response.content));
1271};
1272
1273```
1274
1275And your New Records trigger has a Spreadsheet and a Worksheet dynamic dropdown. The Spreadsheet dynamic dropdown must have a key of `spreadsheet_id`. When the user selects a spreadsheet via the dynamic dropdown the value chosen is made available in `bundle.inputData`. It will then be passed to the Worksheet trigger when the user clicks on the Worksheet dynamic dropdown.
1276
1277```js
1278const App = {
1279 //...
1280 triggers: {
1281 //...
1282 issue: {
1283 key: 'new_records',
1284 //...
1285 create: {
1286 //...
1287 operation: {
1288 inputFields: [
1289 {
1290 key: 'spreadsheet_id',
1291 required: true,
1292 label: 'Spreadsheet',
1293 dynamic: 'spreadsheet.id.name'
1294 },
1295 {
1296 key: 'worksheet_id',
1297 required: true,
1298 label: 'Worksheet',
1299 dynamic: 'worksheet.id.name'
1300 }
1301 ]
1302 }
1303 }
1304 }
1305 }
1306};
1307
1308```
1309
1310The [Google Sheets](https://zapier.com/apps/google-sheets/integrations#triggers-and-actions) integration is an example of this pattern.
1311
1312If you want your trigger to perform specific scripting for a dynamic dropdown you will need to make use of `bundle.meta.isFillingDynamicDropdown`. This can be useful if need to make use of [pagination](#whats-the-deal-with-pagination-when-is-it-used-and-how-does-it-work) in the dynamic dropdown to load more options.
1313
1314```js
1315const App = {
1316 //...
1317 resources: {
1318 project: {
1319 key: 'project',
1320 //...
1321 list: {
1322 //...
1323 operation: {
1324 canPaginate: true,
1325 perform: () => {
1326 if (bundle.meta.isFillingDynamicDropdown) {
1327 // perform pagination request here
1328 } else {
1329 return [{ id: 123, name: 'Project 1' }];
1330 }
1331 }
1332 }
1333 }
1334 },
1335 issue: {
1336 key: 'issue',
1337 //...
1338 create: {
1339 //...
1340 operation: {
1341 inputFields: [
1342 {
1343 key: 'project_id',
1344 required: true,
1345 label: 'Project',
1346 dynamic: 'projectList.id.name'
1347 }, // calls project.list
1348 {
1349 key: 'title',
1350 required: true,
1351 label: 'Title',
1352 helpText: 'What is the name of the issue?'
1353 }
1354 ]
1355 }
1356 }
1357 }
1358 }
1359};
1360
1361```
1362
1363### Search-Powered Fields
1364
1365For fields that take id of another object to create a relationship between the two (EG: a project id for a ticket), you can specify the `search` property on the field to indicate that Zapier needs to prompt the user to setup a Search step to populate the value for this field. Similar to dynamic dropdowns, the value for this property is a dot-separated concatenation of a search's key and the field to use for the value.
1366
1367```js
1368const App = {
1369 //...
1370 resources: {
1371 project: {
1372 key: 'project',
1373 //...
1374 search: {
1375 //...
1376 operation: {
1377 perform: () => {
1378 return [{ id: 123, name: 'Project 1' }];
1379 } // called for project_id
1380 }
1381 }
1382 },
1383 issue: {
1384 key: 'issue',
1385 //...
1386 create: {
1387 //...
1388 operation: {
1389 inputFields: [
1390 {
1391 key: 'project_id',
1392 required: true,
1393 label: 'Project',
1394 dynamic: 'projectList.id.name',
1395 search: 'projectSearch.id'
1396 }, // calls project.search (requires a trigger in the "dynamic" property)
1397 {
1398 key: 'title',
1399 required: true,
1400 label: 'Title',
1401 helpText: 'What is the name of the issue?'
1402 }
1403 ]
1404 }
1405 }
1406 }
1407 }
1408};
1409
1410```
1411
1412**NOTE:** This has to be combined with the `dynamic` property to give the user a guided experience when setting up a Zap.
1413
1414If you don't define a trigger for the `dynamic` property, the search connector won't show.
1415
1416### Computed Fields
1417
1418In OAuth and Session Auth, you may want to use fields from your API call response other than `access_token` or `refresh_token` for OAuth and `sessionKey` for Session auth. If you want to use data from those fields and need Zapier to confirm that they exist, you need to use Computed Fields.
1419
1420Zapier stores every value from an integration's test API call in `bundle.authData` as _Computed Fields_. You can reference these fields in any subsequent API call as needed. If you define a computed field in your integration, Zapier will check to make sure those fields exist when it runs the authentication test API call.
1421
1422Computed fields work like any other field, though with `computed: true` property, and `required: false` as user can not enter computed fields themselves. Reference computed fields in API calls as `{{bundle.authData.field}}`, replacing `field` with that field's name from your test API call response.
1423
1424You can see examples of computed fields in the [OAuth2](#oauth2) or [Session Auth](#session) example sections.
1425
1426### Nested & Children (Line Item) Fields
1427
1428When your action needs to accept an array of items, you can include an input field with the `children` attribute. The `children` attribute accepts a list of [fields](https://zapier.github.io/zapier-platform-schema/build/schema.html#fieldschema) that can be input for each item in this array.
1429
1430```js
1431const App = {
1432 //...
1433 operation: {
1434 //...
1435 inputFields: [
1436 {
1437 key: 'lineItems',
1438 required: true,
1439 children: [
1440 {
1441 key: 'lineItemId',
1442 type: 'integer',
1443 label: 'Line Item ID',
1444 required: true
1445 },
1446 {
1447 key: 'name',
1448 type: 'string',
1449 label: 'Name',
1450 required: true
1451 },
1452 {
1453 key: 'description',
1454 type: 'string',
1455 label: 'Description'
1456 }
1457 ]
1458 }
1459 ]
1460 // ...
1461 }
1462};
1463
1464```
1465
1466## Output Fields
1467
1468On each trigger, search, or create in the operation directive - you can provide an array of objects as fields under the `outputFields`. Output Fields are what your users would see when they select a field provided by your trigger, search or create to map it to another.
1469
1470Output Fields are optional, but can be used to:
1471
1472- Define friendly labels for the returned fields. By default, we will *humanize* for example `my_key` as *My Key*.
1473- Make sure that custom fields that may not be found in every live sample and - since they're custom to the connected account - cannot be defined in the static sample, can still be mapped.
1474
1475The [schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#fieldschema) for `outputFields` is shared with `inputFields` but only the `key` and `required` properties are relevant.
1476
1477Custom/Dynamic Output Fields are defined in the same way as [Custom/Dynamic Input Fields](#customdynamic-fields).
1478
1479### Nested & Children (Line Item) Fields
1480
1481To define an Output Field for a nested field use `{{parent}}__{{key}}`. For children (line item) fields use `{{parent}}[]{{key}}`.
1482
1483```js
1484const recipeOutputFields = (z, bundle) => {
1485 const response = z.request('http://example.com/api/v2/fields.json');
1486 // json is like [{"key":"field_1","label":"Label for Custom Field"}]
1487 return response.then(res => z.JSON.parse(res.content));
1488};
1489
1490const App = {
1491 //...
1492 triggers: {
1493 new_recipe: {
1494 //...
1495 operation: {
1496 perform: () => {},
1497 sample: {
1498 id: 1,
1499 title: 'Pancake',
1500 author: {
1501 id: 1,
1502 name: 'Amy'
1503 },
1504 ingredients: [
1505 {
1506 name: 'Egg',
1507 amount: 1
1508 },
1509 {
1510 name: 'Milk',
1511 amount: 60,
1512 unit: 'g'
1513 },
1514 {
1515 name: 'Flour',
1516 amount: 30,
1517 unit: 'g'
1518 }
1519 ]
1520 },
1521 // an array of objects is the simplest way
1522 outputFields: [
1523 {
1524 key: 'id',
1525 label: 'Recipe ID',
1526 type: 'integer'
1527 },
1528 {
1529 key: 'title',
1530 label: 'Recipe Title',
1531 type: 'string'
1532 },
1533 {
1534 key: 'author__id',
1535 label: 'Author User ID',
1536 type: 'integer'
1537 },
1538 {
1539 key: 'author__name',
1540 label: 'Author Name',
1541 type: 'string'
1542 },
1543 {
1544 key: 'ingredients[]name',
1545 label: 'Ingredient Name',
1546 type: 'string'
1547 },
1548 {
1549 key: 'ingredients[]amount',
1550 label: 'Ingredient Amount',
1551 type: 'number'
1552 },
1553 {
1554 key: 'ingredients[]unit',
1555 label: 'Ingredient Unit',
1556 type: 'string'
1557 },
1558 recipeOutputFields // provide a function inline - we'll merge the results!
1559 ]
1560 }
1561 }
1562 }
1563};
1564
1565```
1566
1567## Z Object
1568
1569We provide several methods off of the `z` object, which is provided as the first argument to all function calls in your app.
1570
1571> The `z` object is passed into your functions as the first argument - IE: `perform: (z) => {}`.
1572
1573### `z.request([url], options)`
1574
1575`z.request([url], options)` is a promise based HTTP client with some Zapier-specific goodies. See [Making HTTP Requests](#making-http-requests).
1576
1577### `z.console`
1578
1579`z.console.log(message)` is a logging console, similar to Node.js `console` but logs remotely, as well as to stdout in tests. See [Log Statements](#console-logging)
1580
1581### `z.dehydrate(func, inputData)`
1582
1583`z.dehydrate(func, inputData)` is used to lazily evaluate a function, perfect to avoid API calls during polling or for reuse. See [Dehydration](#dehydration).
1584
1585### `z.dehydrateFile(func, inputData)`
1586
1587`z.dehydrateFile` is used to lazily download a file, perfect to avoid API calls during polling or for reuse. See [File Dehydration](#file-dehydration).
1588
1589### `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`
1590
1591`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` is a promise based file stasher that returns a URL file pointer. See [Stashing Files](#stashing-files).
1592
1593### `z.JSON`
1594
1595`z.JSON` is similar to the JSON built-in like `z.JSON.parse('...')`, but catches errors and produces nicer tracebacks.
1596
1597### `z.hash()`
1598
1599`z.hash()` is a crypto tool for doing things like `z.hash('sha256', 'my password')`
1600
1601### `z.errors`
1602
1603`z.errors` is a collection error classes that you can throw in your code, like `throw new z.errors.HaltedError('...')`.
1604
1605The available errors are:
1606
1607* HaltedError - Stops current operation, but will never turn off Zap. Read more on [Halting Execution](#halting-execution)
1608* ExpiredAuthError - Turns off Zap and emails user to manually reconnect. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1609* RefreshAuthError - (OAuth2 or Session Auth) Tells Zapier to refresh credentials and retry operation. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1610
1611
1612For more details on error handling in general, see [here](#error-handling).
1613
1614### `z.cursor`
1615
1616The `z.cursor` object exposes two methods:
1617
1618* `z.cursor.get(): Promise<string|null>`
1619* `z.cursor.set(string): Promise<null>`
1620
1621Any data you `set` will be available to that Zap for about an hour (or until it's overwritten). For more information, see: [paging](#paging).
1622
1623## Bundle Object
1624
1625This object holds the user's auth details and the data for the API requests.
1626
1627> The `bundle` object is passed into your functions as the second argument - IE: `perform: (z, bundle) => {}`.
1628
1629### `bundle.authData`
1630
1631`bundle.authData` is user-provided authentication data, like `api_key` or `access_token`. [Read more on authentication.](#authentication)
1632
1633### `bundle.inputData`
1634
1635`bundle.inputData` is user-provided data for this particular run of the trigger/search/create, as defined by the inputFields. For example:
1636
1637```js
1638{
1639 createdBy: 'his name is Bobby Flay'
1640 style: 'he cooks mediterranean'
1641}
1642```
1643
1644### `bundle.inputDataRaw`
1645
1646`bundle.inputDataRaw` is kind of like `inputData`, but before rendering `{{curlies}}`:
1647
1648```js
1649{
1650 createdBy: 'his name is {{123__chef_name}}'
1651 style: 'he cooks {{456__style}}'
1652}
1653```
1654
1655> "curlies" are data mapped in from previous steps. They take the form `{{NODE_ID__key_name}}`. You'll usually want to use `bundle.inputData` instead.
1656
1657### `bundle.meta`
1658
1659`bundle.meta` contains extra information useful for doing advanced behaviors depending on what the user is doing. It has the following options:
1660
1661| key | default | description |
1662| --- | --- | --- |
1663| `isLoadingSample` | `false` | If true, this run was initiated manually via the Zap Editor |
1664| `isFillingDynamicDropdown` | `false` | If true, this poll is being used to populate a dynamic dropdown. You only need to return the fields you specified (such as `id` and `name`), though returning everything is fine too |
1665| `isPopulatingDedupe` | `false` | If true, the results of this poll will be used to initialize the deduplication list rather than trigger a zap. You should grab as many items as possible. See also: [deduplication](#dedup) |
1666| `limit` | `-1` | The number of items you should fetch. `-1` indicates there's no limit. Build this into your calls insofar as you are able |
1667| `page` | `0` | Used in [paging](#paging) to uniquely identify which page of results should be returned |
1668| `isTestingAuth` | `false` | (legacy property) If true, the poll was triggered by a user testing their account (via [clicking "test"](https://cdn.zapier.com/storage/photos/5c94c304ce11b02c073a973466a7b846.png) or during setup). We use this data to populate the auth label, but it's mostly used to verify we made a successful authenticated request |
1669
1670> Before version `8.0.0`, the information in `bundle.meta` was different. See [the old docs](https://github.com/zapier/zapier-platform-cli/blob/a058e6d538a75d215d2e0c52b9f49a97218640c4/README.md#bundlemeta) for the previous values and [the wiki](https://github.com/zapier/zapier-platform-cli/wiki/bundle.meta-changes) for a mapping of old values to new.
1671
1672There's also `bundle.meta.zap.id`, which is only available in the `performSubscribe` and `performUnsubscribe` methods.
1673
1674The user's Zap ID is available during the [subscribe and unsubscribe](https://zapier.github.io/zapier-platform-schema/build/schema.html#basichookoperationschema) methods.
1675
1676For example - you could do:
1677
1678```js
1679const subscribeHook = (z, bundle) => {
1680
1681 const options = {
1682 url: 'http://57b20fb546b57d1100a3c405.mockapi.io/api/hooks',
1683 method: 'POST',
1684 body: {
1685 url: bundle.targetUrl, // bundle.targetUrl has the Hook URL this app should call
1686 zap_id: bundle.meta.zap.id,
1687 },
1688 };
1689
1690 return z.request(options).then((response) => response.json);
1691};
1692
1693module.exports = {
1694 // ... see our rest hook example for additional details: https://github.com/zapier/zapier-platform-example-app-rest-hooks/blob/master/triggers/recipe.js
1695 performSubscribe: subscribeHook,
1696 // ...
1697};
1698```
1699
1700### `bundle.rawRequest`
1701> `bundle.rawRequest` is only available in the `perform` for web hooks and `getAccessToken` for oauth authentication methods.
1702
1703`bundle.rawRequest` holds raw information about the HTTP request that triggered the `perform` method or that represents the users browser request that triggered the `getAccessToken` call:
1704
1705```
1706{
1707 method: 'POST',
1708 querystring: 'foo=bar&baz=qux',
1709 headers: {
1710 'Content-Type': 'application/json'
1711 },
1712 content: '{"hello": "world"}'
1713}
1714```
1715
1716
1717
1718### `bundle.cleanedRequest`
1719> `bundle.cleanedRequest` is only available in the `perform` for webhooks and `getAccessToken` for oauth authentication methods.
1720
1721`bundle.cleanedRequest` will return a formatted and parsed version of the request. Some or all of the following will be available:
1722
1723```
1724{
1725 method: 'POST',
1726 querystring: {
1727 foo: 'bar',
1728 baz: 'qux'
1729 },
1730 headers: {
1731 'Content-Type': 'application/json'
1732 },
1733 content: {
1734 hello: 'world'
1735 }
1736}
1737```
1738
1739### `bundle.targetUrl`
1740
1741> `bundle.targetUrl` is only available in the `performSubscribe` and `performUnsubscribe` methods for webhooks.
1742
1743This the URL to which you should send hook data. It'll look something like `https://hooks.zapier.com/1234/abcd`. We provide it so you can make a POST request to your server.Your server should store this URL and use is as a destination when there's new data to report.
1744
1745Read more in the [REST hook example](https://github.com/zapier/zapier-platform-example-app-rest-hooks/blob/master/triggers/recipe.js).
1746
1747### `bundle.subscribeData`
1748
1749> `bundle.subscribeData` is only available in the `performUnsubscribe` method for webhooks.
1750
1751This is an object that contains the data you returned from the `performSubscribe` function. It should contain whatever information you need send a `DELETE` request to your server to stop sending webhooks to Zapier.
1752
1753Read more in the [REST hook example](https://github.com/zapier/zapier-platform-example-app-rest-hooks/blob/master/triggers/recipe.js).
1754
1755## Environment
1756
1757Apps can define environment variables that are available when the app's code executes. They work just like environment
1758variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you
1759don't want to commit to source control. Environment variables can also be used as a quick way to toggle between a
1760a staging and production environment during app development.
1761
1762It is important to note that **variables are defined on a per-version basis!** When you push a new version, the
1763existing variables from the previous version are copied, so you don't have to manually add them. However, edits
1764made to one version's environment will not affect the other versions.
1765
1766### Defining Environment Variables
1767
1768To define an environment variable, use the `env` command:
1769
1770```bash
1771# Will set the environment variable on Zapier.com
1772zapier env 1.0.0 MY_SECRET_VALUE 1234
1773```
1774
1775You will likely also want to set the value locally for testing.
1776
1777```bash
1778export MY_SECRET_VALUE=1234
1779```
1780
1781Alternatively, we provide some extra tooling to work with an `.env` (or `.environment`, see below note) that looks like this:
1782
1783```
1784MY_SECRET_VALUE=1234
1785```
1786
1787> `.env` is the new recommended name for the environment file since v5.1.0. The old name `.environment` is depreated but will continue to work for backward compatibility.
1788
1789And then in your `test/basic.js` file:
1790
1791```js
1792const zapier = require('zapier-platform-core');
1793
1794should('some tests', () => {
1795 zapier.tools.env.inject(); // testing only!
1796 console.log(process.env.MY_SECRET_VALUE);
1797 // should print '1234'
1798});
1799```
1800
1801> This is a popular way to provide `process.env.ACCESS_TOKEN || bundle.authData.access_token` for convenient testing.
1802
1803> **NOTE** Variables defined via `zapier env` will _always_ be uppercased. For example, you would access the variable defined by `zapier env 1.0.0 foo_bar 1234` with `process.env.FOO_BAR`.
1804
1805
1806### Accessing Environment Variables
1807
1808To view existing environment variables, use the `env` command.
1809
1810```bash
1811# Will print a table listing the variables for this version
1812zapier env 1.0.0
1813```
1814
1815Within your app, you can access the environment via the standard `process.env` - any values set via local `export` or `zapier env` will be there.
1816
1817For example, you can access the `process.env` in your perform functions and in templates:
1818
1819```js
1820const listExample = (z, bundle) => {
1821 const httpOptions = {
1822 headers: {
1823 'my-header': process.env.MY_SECRET_VALUE
1824 }
1825 };
1826 const response = z.request(
1827 'http://example.com/api/v2/recipes.json',
1828 httpOptions
1829 );
1830 return response.then(res => res.json);
1831};
1832
1833const App = {
1834 // ...
1835 triggers: {
1836 example: {
1837 noun: '{{process.env.MY_NOUN}}',
1838 operation: {
1839 // ...
1840 perform: listExample
1841 }
1842 }
1843 }
1844};
1845
1846```
1847
1848> Note! Be sure to lazily access your environment variables - see [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies).
1849
1850
1851## Making HTTP Requests
1852
1853There are two primary ways to make HTTP requests in the Zapier platform:
1854
18551. **Shorthand HTTP Requests** - these are simple object literals that make it easy to define simple requests.
18562. **Manual HTTP Requests** - you use `z.request([url], options)` to make the requests and control the response. Use this when you need to change options for certain requests (for all requests, use middleware).
1857
1858There are also a few helper constructs you can use to reduce boilerplate:
1859
18601. `requestTemplate` which is an shorthand HTTP request that will be merged with every request.
18612. `beforeRequest` middleware which is an array of functions to mutate a request before it is sent.
18623. `afterResponse` middleware which is an array of functions to mutate a response before it is completed.
1863
1864> Note: you can install any HTTP client you like - but this is greatly discouraged as you lose [automatic HTTP logging](#http-logging) and middleware.
1865
1866### Shorthand HTTP Requests
1867
1868For simple HTTP requests that do not require special pre or post processing, you can specify the HTTP options as an object literal in your app definition.
1869
1870This features:
1871
18721. Lazy `{{curly}}` replacement.
18732. JSON and form body de-serialization.
18743. Automatic non-2xx error raising.
1875
1876```js
1877const triggerShorthandRequest = {
1878 method: 'GET',
1879 url: 'http://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
1880 params: {
1881 sort_by: 'id',
1882 sort_order: 'DESC'
1883 }
1884};
1885
1886const App = {
1887 // ...
1888 triggers: {
1889 example: {
1890 // ...
1891 operation: {
1892 // ...
1893 perform: triggerShorthandRequest
1894 }
1895 }
1896 }
1897};
1898
1899```
1900
1901In the URL above, `{{bundle.authData.subdomain}}` is automatically replaced with the live value from the bundle. If the call returns a non 2xx return code, an error is automatically raised. The response body is automatically parsed as JSON and returned.
1902
1903An error will be raised if the response is not valid JSON, so _do not use shorthand HTTP requests with non-JSON responses_.
1904
1905### Manual HTTP Requests
1906
1907When you need to do custom processing of the response, or need to process non-JSON responses, you can make manual HTTP requests. This approach does not perform any magic - no status code checking, no automatic JSON parsing. Use this method when you need more control. Manual requests do perform lazy `{{curly}}` replacement.
1908
1909To make a manual HTTP request, use the `request` method of the `z` object:
1910
1911```js
1912const listExample = (z, bundle) => {
1913 const customHttpOptions = {
1914 headers: {
1915 'my-header': 'from zapier'
1916 }
1917 };
1918
1919 return z
1920 .request('http://example.com/api/v2/recipes.json', customHttpOptions)
1921 .then(response => {
1922 if (response.status >= 300) {
1923 throw new Error(`Unexpected status code ${response.status}`);
1924 }
1925
1926 const recipes = z.JSON.parse(response.content);
1927 // do any custom processing of recipes here...
1928
1929 return recipes;
1930 });
1931};
1932
1933const App = {
1934 // ...
1935 triggers: {
1936 example: {
1937 // ...
1938 operation: {
1939 // ...
1940 perform: listExample
1941 }
1942 }
1943 }
1944};
1945
1946```
1947
1948#### POST and PUT Requests
1949
1950To POST or PUT data to your API you can do this:
1951
1952```js
1953const App = {
1954 // ...
1955 triggers: {
1956 example: {
1957 // ...
1958 operation: {
1959 // ...
1960 perform: (z, bundle) => {
1961 const recipe = {
1962 name: 'Baked Falafel',
1963 style: 'mediterranean',
1964 directions: 'Get some dough....'
1965 };
1966
1967 const options = {
1968 method: 'POST',
1969 body: JSON.stringify(recipe)
1970 };
1971
1972 return z
1973 .request('http://example.com/api/v2/recipes.json', options)
1974 .then(response => {
1975 if (response.status !== 201) {
1976 throw new Error(`Unexpected status code ${response.status}`);
1977 }
1978 });
1979 }
1980 }
1981 }
1982 }
1983};
1984
1985```
1986
1987> Note: you need to call `z.JSON.stringify()` before setting the `body`.
1988
1989### Using HTTP middleware
1990
1991If you need to process all HTTP requests in a certain way, you may be able to use one of utility HTTP middleware functions.
1992
1993> Example App: check out https://github.com/zapier/zapier-platform-example-app-middleware for a working example app using HTTP middleware.
1994
1995Try putting them in your app definition:
1996
1997```js
1998const addHeader = (request, z, bundle) => {
1999 request.headers['my-header'] = 'from zapier';
2000 return request;
2001};
2002
2003const mustBe200 = (response, z, bundle) => {
2004 if (response.status !== 200) {
2005 throw new Error(`Unexpected status code ${response.status}`);
2006 }
2007 return response;
2008};
2009
2010const autoParseJson = (response, z, bundle) => {
2011 response.json = z.JSON.parse(response.content);
2012 return response;
2013};
2014
2015const App = {
2016 // ...
2017 beforeRequest: [addHeader],
2018 afterResponse: [mustBe200, autoParseJson]
2019 // ...
2020};
2021
2022```
2023
2024A `beforeRequest` middleware function takes a request options object, and returns a (possibly mutated) request object. An `afterResponse` middleware function takes a response object, and returns a (possibly mutated) response object. Middleware functions are executed in the order specified in the app definition, and each subsequent middleware receives the request or response object returned by the previous middleware.
2025
2026Middleware functions can be asynchronous - just return a promise from the middleware function.
2027
2028The second argument for middleware is the `z` object, but it does *not* include `z.request()` as using that would easily create infinite loops.
2029
2030### HTTP Request Options
2031
2032Shorthand requests and manual `z.request([url], options)` calls support the following HTTP `options`:
2033
2034* `url`: HTTP url, you can provide it both `z.request(url, options)` or `z.request({url: url, ...})`.
2035* `method`: HTTP method, default is `GET`.
2036* `headers`: request headers object, format `{'header-key': 'header-value'}`.
2037* `params`: URL query params object, format `{'query-key': 'query-value'}`.
2038* `body`: request body, can be a string, buffer, readable stream or plain object. When it is an object/array and the `Content-Type` header is `application/x-www-form-urlencoded` the body will be transformed to query string parameters, otherwise we'll set the header to `application/json; charset=utf-8` and JSON encode the body. Default is `null`.
2039* `json`: shortcut object/array/etc. you want to JSON encode into body. Default is `null`.
2040* `form`: shortcut object. you want to form encode into body. Default is `null`.
2041* `raw`: set this to stream the response instead of consuming it immediately. Default is `false`.
2042* `redirect`: set to `manual` to extract redirect headers, `error` to reject redirect, default is `follow`.
2043* `follow`: maximum redirect count, set to `0` to not follow redirects. default is `20`.
2044* `compress`: support gzip/deflate content encoding. Set to `false` to disable. Default is `true`.
2045* `agent`: Node.js `http.Agent` instance, allows custom proxy, certificate etc. Default is `null`.
2046* `timeout`: request / response timeout in ms. Set to `0` to disable (OS limit still applies), timeout reset on `redirect`. Default is `0` (disabled).
2047* `size`: maximum response body size in bytes. Set to `0` to disable. Default is `0` (disabled).
2048
2049```js
2050z.request({
2051 url: 'http://example.com',
2052 method: 'POST',
2053 headers: {
2054 'Content-Type': 'application/json'
2055 },
2056 // only provide body, json or form...
2057 body: {hello: 'world'}, // or '{"hello": "world"}' or 'hello=world'
2058 json: {hello: 'world'},
2059 form: {hello: 'world'},
2060 // access node-fetch style response.body
2061 raw: false,
2062 redirect: 'follow',
2063 follow: 20,
2064 compress: true,
2065 agent: null,
2066 timeout: 0,
2067 size: 0,
2068})
2069```
2070
2071### HTTP Response Object
2072
2073The response object returned by `z.request([url], options)` supports the following fields and methods:
2074
2075* `status`: The response status code, i.e. `200`, `404`, etc.
2076* `content`: The response content as a String. For Buffer, try `options.raw = true`.
2077* `json`: The response content as an object (or `undefined`). If `options.raw = true` - is a promise.
2078* `body`: A stream available only if you provide `options.raw = true`.
2079* `headers`: Response headers object. The header keys are all lower case.
2080* `getHeader(key)`: Retrieve response header, case insensitive: `response.getHeader('My-Header')`
2081* `throwForStatus()`: Throw error if final `response.status > 300`. Will throw `z.error.RefreshAuthError` if 401.
2082* `request`: The original request options object (see above).
2083
2084```js
2085z.request({
2086 // ..
2087}).then((response) => {
2088 // a bunch of examples lines for cherry picking
2089 response.status;
2090 response.headers['Content-Type'];
2091 response.getHeader('content-type');
2092 response.request; // original request options
2093 response.throwForStatus();
2094 // if options.raw === false (default)...
2095 JSON.parse(response.content);
2096 response.json;
2097 // if options.raw === true...
2098 response.buffer().then(buf => buf.toString());
2099 response.text().then(content => content);
2100 response.json().then(json => json);
2101 response.body.pipe(otherStream);
2102});
2103```
2104
2105
2106## Dehydration
2107
2108Dehydration, and its counterpart Hydration, is a tool that can lazily load data that might be otherwise expensive to retrieve aggressively.
2109
2110* **Dehydration** - think of this as "make a pointer", you control the creation of pointers with `z.dehydrate(func, inputData)` (or `z.dehydrateFile(func, inputData)` for files). This usually happens in a trigger step.
2111* **Hydration** - think of this as an automatic step that "consumes a pointer" and "returns some data", Zapier does this automatically behind the scenes. This usually happens in an action step.
2112
2113> This is very common when [Stashing Files](#stashing-files) - but that isn't their only use!
2114
2115The method `z.dehydrate(func, inputData)` has two required arguments:
2116
2117* `func` - the function to call to fetch the extra data. Can be any raw `function`, defined in the file doing the dehydration or imported from another part of your app. You must also register the function in the app's `hydrators` property
2118* `inputData` - this is an object that contains things like a `path` or `id` - whatever you need to load data on the other side
2119
2120> **Why do I need to register my functions?** Because of how Javascript works with its module system, we need an explicit handle on the function that can be accessed from the App definition without trying to "automagically" (and sometimes incorrectly) infer code locations.
2121
2122Here is an example that pulls in extra data for a movie:
2123
2124```js
2125const getExtraDataFunction = (z, bundle) => {
2126 const url = `http://example.com/movies/${bundle.inputData.id}.json`;
2127 return z.request(url).then(res => z.JSON.parse(res.content));
2128};
2129
2130const movieList = (z, bundle) => {
2131 return z
2132 .request('http://example.com/movies.json')
2133 .then(res => z.JSON.parse(res.content))
2134 .then(results => {
2135 return results.map(result => {
2136 // so maybe /movies.json is thin content but
2137 // /movies/:id.json has more details we want...
2138 result.moreData = z.dehydrate(getExtraDataFunction, {
2139 id: result.id
2140 });
2141 return result;
2142 });
2143 });
2144};
2145
2146const App = {
2147 version: require('./package.json').version,
2148 platformVersion: require('zapier-platform-core').version,
2149
2150 // don't forget to register hydrators here!
2151 // it can be imported from any module
2152 hydrators: {
2153 getExtraData: getExtraDataFunction
2154 },
2155
2156 triggers: {
2157 new_movie: {
2158 noun: 'Movie',
2159 display: {
2160 label: 'New Movie',
2161 description: 'Triggers when a new Movie is added.'
2162 },
2163 operation: {
2164 perform: movieList
2165 }
2166 }
2167 }
2168};
2169
2170module.exports = App;
2171
2172```
2173
2174And in future steps of the Zap - if Zapier encounters a pointer as returned by `z.dehydrate(func, inputData)` - Zapier will tie it back to your app and pull in the data lazily.
2175
2176> **Why can't I just load the data immediately?** Isn't it easier? In some cases it can be - but imagine an API that returns 100 records when polling - doing 100x `GET /id.json` aggressive inline HTTP calls when 99% of the time Zapier doesn't _need_ the data yet is wasteful.
2177
2178
2179### File Dehydration
2180
2181*New in v7.3.0.*
2182
2183The method `z.dehydrateFile(func, inputData)` allows you to download a file lazily. It takes the identical arguments as `z.dehydrate(func, inputData)` does.
2184
2185An example can be found in the [Stashing Files](#stashing-files) section.
2186
2187What makes `z.dehydrateFile` different from `z.dehydrate` has to do with efficiency and when Zapier chooses to hydrate data. Knowing which pointers give us back files helps us delay downloading files until its absolutely necessary. A good example is users creating Zaps in the Zap Editor. If a pointer is made by `z.dehydrate`, the Zap Editor will hydrate the data immediately after pulling in samples. This allows users to map fields from the hydrated data into the subsequent steps of the Zap. If, however, the pointer is made by `z.dehydrateFile`, the Zap Editor will wait to hydrate the file. There's nothing in binary file data for users to map in the subsequent steps.
2188
2189> `z.dehydrateFile(func, inputData)` is new in v7.3.0. We used to recommend to use `z.dehydrate(func, inputData)` for files, but it's not the case anymore. Please change it to `z.dehydrateFile(func, inputData)` for a better user expereience.
2190
2191## Stashing Files
2192
2193It can be expensive to download and stream files or they can require complex handshakes to authorize downloads - so we provide a helpful stash routine that will take any `String`, `Buffer` or `Stream` and return a URL file pointer suitable for returning from triggers, searches, creates, etc.
2194
2195The interface `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` takes a single required argument - the extra three arguments will be automatically populated in most cases. For example - a full example is this:
2196
2197```js
2198const content = 'Hello world!';
2199z.stashFile(content, content.length, 'hello.txt', 'text/plain')
2200 .then(url => z.console.log(url));
2201// https://zapier-dev-files.s3.amazonaws.com/cli-platform/f75e2819-05e2-41d0-b70e-9f8272f9eebf
2202```
2203
2204Most likely you'd want to stream from another URL - note the usage of `z.request({raw: true})`:
2205
2206```js
2207const fileRequest = z.request({url: 'http://example.com/file.pdf', raw: true});
2208z.stashFile(fileRequest) // knownLength and filename will be sniffed from the request. contentType will be binary/octet-stream
2209 .then(url => z.console.log(url));
2210// https://zapier-dev-files.s3.amazonaws.com/cli-platform/74bc623c-d94d-4cac-81f1-f71d7d517bc7
2211```
2212
2213> Note: you should only be using `z.stashFile()` in a hydration method or a hook trigger's `perform` if you're sending over a short-lived URL to a file. Otherwise, it can be very expensive to stash dozens of files in a polling call - for example!
2214
2215See a full example with dehydration/hydration wired in correctly:
2216
2217```js
2218const stashPDFfunction = (z, bundle) => {
2219 // use standard auth to request the file
2220 const filePromise = z.request({
2221 url: bundle.inputData.downloadUrl,
2222 raw: true
2223 });
2224 // and swap it for a stashed URL
2225 return z.stashFile(filePromise);
2226};
2227
2228const pdfList = (z, bundle) => {
2229 return z
2230 .request('http://example.com/pdfs.json')
2231 .then(res => z.JSON.parse(res.content))
2232 .then(results => {
2233 return results.map(result => {
2234 // lazily convert a secret_download_url to a stashed url
2235 // zapier won't do this until we need it
2236 result.file = z.dehydrateFile(stashPDFfunction, {
2237 downloadUrl: result.secret_download_url
2238 });
2239 delete result.secret_download_url;
2240 return result;
2241 });
2242 });
2243};
2244
2245const App = {
2246 version: require('./package.json').version,
2247 platformVersion: require('zapier-platform-core').version,
2248
2249 hydrators: {
2250 stashPDF: stashPDFfunction
2251 },
2252
2253 triggers: {
2254 new_pdf: {
2255 noun: 'PDF',
2256 display: {
2257 label: 'New PDF',
2258 description: 'Triggers when a new PDF is added.'
2259 },
2260 operation: {
2261 perform: pdfList
2262 }
2263 }
2264 }
2265};
2266
2267module.exports = App;
2268
2269```
2270
2271> Example App: check out https://github.com/zapier/zapier-platform-example-app-files for a working example app using files.
2272
2273
2274## Logging
2275
2276There are two types of logs for a Zapier app, console logs and HTTP logs. The console logs are created by your app through the use of the `z.console.log` method ([see below for details](#console-logging)). The HTTP logs are created automatically by Zapier whenever your app makes HTTP requests (as long as you use `z.request([url], options)` or shorthand request objects).
2277
2278To view the logs for your application, use the `zapier logs` command. There are three types of logs, `http` (logged automatically by Zapier on HTTP requests), `bundle` (logged automatically on every method execution), and `console` (manual logs via `z.console.log()` statements).
2279
2280For advanced logging options including only displaying the logs for a certain user or app version, look at the help for the logs command:
2281
2282```bash
2283zapier help logs
2284```
2285
2286### Console Logging
2287
2288To manually print a log statement in your code, use `z.console.log`:
2289
2290```js
2291z.console.log('Here are the input fields', bundle.inputData);
2292```
2293
2294The `z.console` object has all the same methods and works just like the Node.js [`Console`](https://nodejs.org/docs/latest-v6.x/api/console.html) class - the only difference is we'll log to our distributed datastore and you can view them via `zapier logs` (more below).
2295
2296### Viewing Console Logs
2297
2298To see your `z.console.log` logs, do:
2299
2300```bash
2301zapier logs --type=console
2302```
2303
2304### Viewing Bundle Logs
2305
2306To see the bundle logs, do:
2307
2308```bash
2309zapier logs --type=bundle
2310```
2311
2312### HTTP Logging
2313
2314If you are using the `z.request()` shortcut that we provide - HTTP logging is handled automatically for you. For example:
2315
2316```js
2317z.request('http://57b20fb546b57d1100a3c405.mockapi.io/api/recipes')
2318 .then((res) => {
2319 // do whatever you like, this request is already getting logged! :-D
2320 return res;
2321 })
2322```
2323
2324### Viewing HTTP Logs
2325
2326To see the HTTP logs, do:
2327
2328```bash
2329zapier logs --type=http
2330```
2331To see detailed http logs including headers, request and response bodies, etc, do:
2332
2333```bash
2334zapier logs --type=http --detailed
2335```
2336
2337
2338## Error Handling
2339
2340APIs are not always available. Users do not always input data correctly to
2341formulate valid requests. Thus, it is a good idea to write apps defensively and
2342plan for 4xx and 5xx responses from APIs. Without proper handling, errors often
2343have incomprehensible messages for end users, or possibly go uncaught.
2344
2345Zapier provides a couple tools to help with error handling. First is the `afterResponse`
2346middleware ([docs](#using-http-middleware)), which provides a hook for processing
2347all responses from HTTP calls. The other tool is the collection of errors in
2348`z.errors` ([docs](#zerrors)), which control the behavior of Zaps when
2349various kinds of errors occur.
2350
2351### General Errors
2352
2353Errors due to a misconfiguration in a user's Zap should be handled in your app
2354by throwing a standard JavaScript `Error` with a user-friendly message.
2355Typically, this will be prettifying 4xx responses or APIs that return errors as
2356200s with a payload that describes the error.
2357
2358Example: `throw new Error('Your error message.');`
2359
2360A couple best practices to keep in mind:
2361
2362 * Elaborate on terse messages. "not_authenticated" -> "Your API Key is invalid. Please reconnect your account."
2363 * If the error calls out a specific field, surface that information to the user. "Invalid Request" -> "contact name is invalid"
2364 * If the error provides details about why a field is invalid, add that in too! "contact name is invalid" -> "contact name is too long"
2365
2366Note that if a Zap raises too many error messages it will be automatically
2367turned off, so only use these if the scenario is truly an error that needs to
2368be fixed.
2369
2370### Halting Execution
2371
2372Any operation can be interrupted or "halted" (not success, not error, but
2373stopped for some specific reason) with a `HaltedError`. You might find yourself
2374using this error in cases where a required pre-condition is not met. For instance,
2375in a create to add an email address to a list where duplicates are not allowed,
2376you would want to throw a `HaltedError` if the Zap attempted to add a duplicate.
2377This would indicate failure, but it would be treated as a soft failure.
2378
2379Unlike throwing `Error`, a Zap will never by turned off when this error is thrown
2380(even if it is raised more often than not).
2381
2382Example: `throw new z.errors.HaltedError('Your reason.');`
2383
2384### Stale Authentication Credentials
2385
2386For apps that require manual refresh of authorization on a regular basis, Zapier
2387provides a mechanism to notify users of expired credentials. With the
2388`ExpiredAuthError`, the current operation is interrupted, the Zap is turned off
2389(to prevent more calls with expired credentials), and a predefined email is sent
2390out informing the user to refresh the credentials.
2391
2392Example: `throw new z.errors.ExpiredAuthError('Your message.');`
2393
2394For apps that use OAuth2 + refresh or Session Auth, you can use the
2395`RefreshAuthError`. This will signal Zapier to refresh the credentials and then
2396repeat the failed operation.
2397
2398Example: `throw new z.errors.RefreshAuthError();`
2399
2400
2401## Testing
2402
2403You can write unit tests for your Zapier app that run locally, outside of the zapier editor.
2404You can run these tests in a CI tool like [Travis](https://travis-ci.com/).
2405
2406### Writing Unit Tests
2407
2408We recommend using the [Mocha](https://mochajs.org/) testing framework. After running
2409`zapier init` you should find an example test to start from in the `test` directory.
2410
2411```js
2412// we use should assertions
2413const should = require('should');
2414const zapier = require('zapier-platform-core');
2415
2416// createAppTester() makes it easier to test your app. It takes your
2417// raw app definition, and returns a function that will test you app.
2418const App = require('../index');
2419const appTester = zapier.createAppTester(App);
2420
2421describe('triggers', () => {
2422 describe('new recipe trigger', () => {
2423 it('should load recipes', done => {
2424 // This is what Zapier will send to your app as input.
2425 // It contains trigger options the user choice in their zap.
2426 const bundle = {
2427 inputData: {
2428 style: 'mediterranean'
2429 }
2430 };
2431
2432 // Pass appTester the path to the trigger you want to call,
2433 // and the input bundle. appTester returns a promise for results.
2434 appTester(App.App.triggers.recipe.operation.perform, bundle)
2435 .then(results => {
2436 // Make assertions
2437
2438 results.length.should.eql(10);
2439
2440 const firstRecipe = results[0];
2441 firstRecipe.name.should.eql('Baked Falafel');
2442
2443 done();
2444 })
2445 .catch(done);
2446 });
2447 });
2448});
2449
2450```
2451
2452### Mocking Requests
2453
2454While testing, it's useful to test your code without actually hitting any external services. [Nock](https://github.com/node-nock/nock) is a node.js utility that intercepts requests before they ever leave your computer. You can specify a response code, body, headers, and more. It works out of the box with `z.request` by setting up your `nock` before calling `appTester`.
2455
2456```js
2457require('should');
2458
2459const zapier = require('zapier-platform-core');
2460
2461const App = require('../index');
2462const appTester = zapier.createAppTester(App);
2463
2464const nock = require('nock');
2465
2466describe('triggers', () => {
2467 describe('new recipe trigger', () => {
2468 it('should load recipes', done => {
2469 const bundle = {
2470 inputData: {
2471 style: 'mediterranean'
2472 }
2473 };
2474
2475 // mocks the next request that matches this url and querystring
2476 nock('http://57b20fb546b57d1100a3c405.mockapi.io/api')
2477 .get('/recipes')
2478 .query(bundle.inputData)
2479 .reply(200, [
2480 { name: 'name 1', directions: 'directions 1', id: 1 },
2481 { name: 'name 2', directions: 'directions 2', id: 2 }
2482 ]);
2483
2484 appTester(App.triggers.recipe.operation.perform, bundle)
2485 .then(results => {
2486 results.length.should.above(1);
2487
2488 const firstRecipe = results[0];
2489 firstRecipe.name.should.eql('name 1');
2490 firstRecipe.directions.should.eql('directions 1');
2491
2492 done();
2493 })
2494 .catch(done);
2495 });
2496
2497 it('should load recipes without filters', done => {
2498 const bundle = {};
2499
2500 // each test needs its own mock
2501 nock('http://57b20fb546b57d1100a3c405.mockapi.io/api')
2502 .get('/recipes')
2503 .reply(200, [
2504 { name: 'name 1', directions: 'directions 1', id: 1 },
2505 { name: 'name 2', directions: 'directions 2', id: 2 }
2506 ]);
2507
2508 appTester(App.triggers.recipe.operation.perform, bundle)
2509 .then(results => {
2510 results.length.should.above(1);
2511
2512 const firstRecipe = results[0];
2513 firstRecipe.name.should.eql('name 1');
2514 firstRecipe.directions.should.eql('directions 1');
2515
2516 done();
2517 })
2518 .catch(done);
2519 });
2520 });
2521});
2522
2523```
2524
2525There's more info about nock and its usage in its [readme](https://github.com/node-nock/nock/blob/master/README.md).
2526
2527### Running Unit Tests
2528
2529To run all your tests do:
2530
2531```bash
2532zapier test
2533```
2534
2535> You can also go direct with `npm test` or `node_modules/mocha/bin/mocha`.
2536
2537### Testing & Environment Variables
2538
2539The best way to store sensitive values (like API keys, OAuth secrets, or passwords) is in an `.env` (or `.environment`, see below note) file ([learn more](https://github.com/motdotla/dotenv#faq)). Then, you can include the following before your tests run:
2540
2541```js
2542const zapier = require('zapier-platform-core');
2543zapier.tools.env.inject(); // inject() can take a filename; defaults to ".env"
2544
2545// now process.env has all the values in your .env file
2546```
2547
2548> `.env` is the new recommended name for the environment file since v5.1.0. The old name `.environment` is depreated but will continue to work for backward compatibility.
2549
2550> Remember: **NEVER** add your secrets file to version control!
2551
2552Additionally, you can provide them dynamically at runtime:
2553
2554```bash
2555CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2556```
2557
2558Or, `export` them explicitly and place them into the environment:
2559
2560```bash
2561export CLIENT_ID=1234
2562export CLIENT_SECRET=abcd
2563zapier test
2564```
2565
2566
2567### Viewing HTTP Logs in Unit Tests
2568
2569
2570When running a unit test via `zapier test`, `z.console` statements and detailed HTTP logs print to `stdout`:
2571
2572```bash
2573zapier test
2574```
2575
2576Sometimes you don't want that much logging, for example in a CI test. To suppress the detailed HTTP logs do:
2577
2578```bash
2579zapier test --quiet
2580```
2581
2582To also suppress the HTTP summary logs do:
2583
2584```bash
2585zapier test --very-quiet
2586```
2587
2588### Testing in Your CI
2589
2590Whether you use Travis, Circle, Jenkins, or anything else, we aim to make it painless to test in an automated environment.
2591
2592Behind the scenes `zapier test` is doing a pretty standard `npm test` with [mocha](https://www.npmjs.com/package/mocha) as the backend.
2593
2594This makes it pretty straightforward to integrate into your testing interface. If you'd like to test with [Travis CI](https://travis-ci.com/) for example - the `.travis.yml` would look something like this:
2595
2596```yaml
2597language: node_js
2598node_js:
2599 - "v8.10.0"
2600before_script: npm install -g zapier-platform-cli
2601script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2602```
2603
2604You can substitute `zapier test` with `npm test`, or a direct call to `node_modules/mocha/bin/mocha`. Also, we generally recommend putting the environment variables into whatever configuration screen Jenkins or Travis provides!
2605
2606As an alternative to reading the deploy key from root (the default location), you may set the `ZAPIER_DEPLOY_KEY` environment variable to run privileged commands without the human input needed for `zapier login`. We suggest encrypting your deploy key in whatever manner you CI provides (such as [these instructions](https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml), for Travis).
2607
2608
2609## Using `npm` Modules
2610
2611Use `npm` modules just like you would use them in any other node app, for example:
2612
2613```bash
2614npm install --save jwt
2615```
2616
2617And then `package.json` will be updated, and you can use them like anything else:
2618
2619```js
2620const jwt = require('jwt');
2621```
2622
2623During the `zapier build` or `zapier push` step - we'll copy all your code to `/tmp` folder and do a fresh re-install of modules.
2624
2625> Note: If your package isn't being pushed correctly (IE: you get "Error: Cannot find module 'whatever'" in production), try adding the `--disable-dependency-detection` flag to `zapier push`.
2626
2627> Note 2: You can also try adding a "includeInBuild" array property (with paths to include, which will be evaluated to RegExp, with a case insensitive flag) to your `.zapierapprc` file, to make it look like:
2628
2629```json
2630{
2631 "id": 1,
2632 "key": "App1",
2633 "includeInBuild": [
2634 "test.txt",
2635 "testing.json"
2636 ]
2637}
2638
2639```
2640
2641> Warning: do not use compiled libraries unless you run your build on the AWS AMI `ami-4fffc834`, or follow the Docker instructions below.
2642
2643
2644## Building Native Packages with Docker
2645
2646Unfortunately if you are developing on a macOS or Windows box you won't be able to build native libraries locally. If you try and push locally build native modules, you'll get runtime errors during usage. However, you can use Docker and Docker Compose to do this in a pinch. Make sure you have all the necessary Docker programs installed and follow along.
2647
2648First, create your `Dockerfile`:
2649
2650```Dockerfile
2651FROM amazonlinux:2017.03.1.20170812
2652
2653RUN yum install zip findutils wget gcc44 gcc-c++ libgcc44 cmake -y
2654
2655RUN wget https://nodejs.org/dist/v8.10.0/node-v8.10.0.tar.gz && \
2656 tar -zxvf node-v8.10.0.tar.gz && \
2657 cd node-v8.10.0 && \
2658 ./configure && \
2659 make && \
2660 make install && \
2661 cd .. && \
2662 rm -rf node-v8.10.0 node-v8.10.0.tar.gz
2663
2664RUN npm i -g zapier-platform-cli
2665
2666WORKDIR /app
2667```
2668
2669And finally, create your `docker-compose.yml` file:
2670
2671```yml
2672version: '3.4'
2673
2674services:
2675 pusher:
2676 build: .
2677 volumes:
2678 - .:/app
2679 - node_modules:/app/node_modules:delegated
2680 - ~/.zapierrc:/root/.zapierrc
2681 command: 'bash -c "npm i && zapier push"'
2682 environment:
2683 ZAPIER_DEPLOY_KEY: ${ZAPIER_DEPLOY_KEY}
2684
2685volumes:
2686 node_modules:
2687```
2688
2689> Note: watch out for your `package-lock.json` file, if it exists for local install it might incorrectly pin a native version.
2690
2691Now you should be able to run `docker-compose run pusher` and see the build and push successfully complete!
2692
2693
2694## Using Transpilers
2695
2696We do not yet support transpilers out of the box, but if you would like to use `babel` or similar, we recommend creating a custom wrapper on `zapier push` like this in your `package.json`:
2697
2698```json
2699{
2700 "scripts": {
2701 "zapier-dev": "babel src --out-dir lib --watch",
2702 "zapier-push": "babel src --out-dir lib && zapier push"
2703 }
2704}
2705```
2706
2707And then you can have your fancy ES7 code in `src/*` and a root `index.js` like this:
2708
2709```js
2710module.exports = require('./lib');
2711```
2712
2713And work with commands like this:
2714
2715```bash
2716# watch and recompile
2717npm run zapier-dev
2718
2719# tests should work fine
2720zapier test
2721
2722# every build ensures a fresh build
2723npm run zapier-push
2724```
2725
2726There are a lot of details left out - check out the full example app for a working setup.
2727
2728> Example App: check out https://github.com/zapier/zapier-platform-example-app-babel for a working example app using Babel.
2729
2730## Example Apps
2731
2732See [the wiki](https://github.com/zapier/zapier-platform-cli/wiki/Example-Apps) for a full list of working examples (and installation instructions).
2733
2734## FAQs
2735
2736### Why doesn't Zapier support newer versions of Node.js?
2737
2738We run your code on AWS Lambda, which only supports a few [versions](https://docs.aws.amazon.com/lambda/latest/dg/programming-model.html) of Node (the latest of which is `v8.10.0`. As that updates, so too will we.
2739
2740### How do I manually set the Node.js version to run my app with?
2741
2742Update your `zapier-platform-core` dependency in `package.json`. Each major version ties to a specific version of Node.js. You can find the mapping [here](https://github.com/zapier/zapier-platform-cli/blob/master/src/version-store.js). We only support the version(s) supported by [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/programming-model.html).
2743
2744### When to use placeholders or curlies?
2745
2746You will see both [template literal placeholders](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Expression_interpolation) `${var}` and (double) "curlies" `{{var}}` used in examples.
2747
2748In general, use `${var}` within functions and use `{{var}}` anywhere else.
2749
2750Placeholders get evaluated as soon as the line of code is evaluated. This means that if you use `${process.env.VAR}` in a trigger configuration, `zapier push` will substitute it with your local environment's value for `VAR` when it builds your app and the value set via `zapier env` will not be used.
2751
2752> If you're not familiar with [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals), know that `const val = "a" + b + "c"` is essentially the same as <code>const val = &#96;a${b}c&#96;</code>.
2753
2754### Does Zapier support XML (SOAP) APIs?
2755
2756Not natively, but it can! Users have reported that the following `npm` modules are compatible with the CLI Platform:
2757
2758* [pixl-xml](https://github.com/jhuckaby/pixl-xml)
2759* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)
2760* [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
2761
2762```js
2763const xml = require('pixl-xml');
2764
2765const App = {
2766 // ...
2767 afterResponse: [
2768 (response, z, bundle) => {
2769 response.xml = xml.parse(response.content);
2770 return response;
2771 }
2772 ]
2773 // ...
2774};
2775
2776```
2777
2778### Is it possible to iterate over pages in a polling trigger?
2779
2780Yes, though there are caveats. Your entire function only gets 30 seconds to run. HTTP requests are costly, so paging through a list may time out (which you should avoid at all costs).
2781
2782```js
2783// some async call
2784const makeCall = (z, start, limit) => {
2785 return z.request({
2786 url: 'https://jsonplaceholder.typicode.com/posts',
2787 params: {
2788 _start: start,
2789 _limit: limit
2790 }
2791 });
2792};
2793
2794// triggers on paging with a certain tag
2795const performPaging = (z, bundle) => {
2796 const limit = 3;
2797 let start = 0;
2798
2799 // array of promises
2800 let promises = [];
2801
2802 let i = 0;
2803 while (i < 5) {
2804 promises.push(makeCall(z, start, limit));
2805 start += limit;
2806 i += 1;
2807 }
2808
2809 return Promise.all(promises).then(res => {
2810 // res is an array of responses
2811 const results = res.map(r => r.json); // array of arrays of js objects
2812 return Array.prototype.concat.apply([], results); // flatten array
2813 });
2814};
2815
2816module.exports = {
2817 key: 'paging',
2818 noun: 'Paging',
2819
2820 display: {
2821 label: 'Get Paging',
2822 description: 'Triggers on a new paging.'
2823 },
2824
2825 operation: {
2826 inputFields: [],
2827 perform: performPaging
2828 }
2829};
2830
2831```
2832
2833If you need to do more requests conditionally based on the results of an HTTP call (such as the "next URL" param or similar value), using `async/await` (as shown in the example below) is a good way to go. If you go this route, only page as far as you need to. Keep an eye on the polling [guidelines](https://zapier.com/developer/documentation/v2/deduplication/), namely the part about only iterating until you hit items that have probably been seen in a previous poll.
2834
2835```js
2836// a hypothetical API where payloads are big so we want to heavily limit how much comes back
2837// we want to only return items created in the last hour
2838
2839const asyncExample = async (z, bundle) => {
2840 const limit = 3;
2841 let start = 0;
2842 const twoHourMilliseconds = 60 * 60 * 2 * 1000;
2843 const hoursAgo = new Date() - twoHourMilliseconds;
2844
2845 let response = await z.request({
2846 url: 'https://jsonplaceholder.typicode.com/posts',
2847 params: {
2848 _start: start,
2849 _limit: limit
2850 }
2851 });
2852
2853 let results = response.json;
2854
2855 // keep paging until the last item was created over two hours ago
2856 // then we know we almost certainly haven't missed anything and can let
2857 // deduper handle the rest
2858
2859 while (new Date(results[results.length - 1].createdAt) > hoursAgo) {
2860 start += limit; // next page
2861
2862 response = await z.request({
2863 url: 'https://jsonplaceholder.typicode.com/posts',
2864 params: {
2865 _start: start,
2866 _limit: limit
2867 }
2868 });
2869
2870 results = results.concat(response.json);
2871 }
2872
2873 return results;
2874};
2875
2876```
2877
2878### How do search-powered fields relate to dynamic dropdowns and why are they both required together?
2879
2880To understand search-powered fields, we have to have a good understanding of dynamic dropdowns.
2881
2882When users are selecting specific resources (for instance, a Google Sheet), it's important they're able to select the exact sheet they want. Instead of referencing the sheet by name (which may change), we match via `id` instead. Rather than directing the user copy and paste an id for every item they might encounter, there is the notion of a **dynamic dropdown**. A dropdown is a trigger that returns a list of resources. It can pull double duty and use its results to power another trigger, search, or action in the same app. It provides a list of ids with labels that show the item's name:
2883
2884![](https://cdn.zapier.com/storage/photos/fb56bdc2aab91504be0e51800bec4d64.png)
2885
2886The field's value reaches your app as an id. You define this connection with the `dynamic` property, which is a string: `trigger_key.id_key.label_key`. This approach works great if the user setting up the Zap always wants the Zap to use the same spreadsheet. They specify the id during setup and the Zap runs happily.
2887
2888**Search fields** take this connection a step further. Rather than set the spreadsheet id at setup, the user could precede the action with a search field to make the id dynamic. For instance, let's say you have a different spreadsheet for every day of the week. You could build the following zap:
2889
28901. Some Trigger
28912. Calculate what day of the week it is today (Code)
28923. Find the spreadsheet that matches the day from Step 2
28934. Update the spreadsheet (with the id from step 3) with some data
2894
2895If the connection between steps 3 and 4 is a common one, you can indicate that in your field by specifying `search` as a `search_key.id_key`. When paired **with a dynamic dropdown**, this will add a button to the editor that will add the search step to the user's Zap and map the id field correctly.
2896
2897![](https://cdn.zapier.com/storage/photos/d263fd3a56cf8108cb89195163e7c9aa.png)
2898
2899This is paired most often with "update" actions, where a required parameter will be a resource id.
2900
2901<a id="paging"></a>
2902### What's the deal with pagination? When is it used and how does it work?
2903
2904Paging is **only used when a trigger is part of a dynamic dropdown**. Depending on how many items exist and how many are returned in the first poll, it's possible that the resource the user is looking for isn't in the initial poll. If they hit the "see more" button, we'll increment the value of `bundle.meta.page` and poll again.
2905
2906Paging is a lot like a regular trigger except the range of items returned is dynamic. The most common example of this is when you can pass a `offset` parameter:
2907
2908```js
2909(z, bundle) => {
2910 const promise = z.request({
2911 url: 'http://example.com/api/list.json',
2912 params: {
2913 limit: 100,
2914 offset: 100 * bundle.meta.page
2915 }
2916 });
2917 return promise.then((response) => response.json);
2918};
2919```
2920
2921If your API uses cursor-based paging instead of an offset, you can use `z.cursor.get` and `z.cursor.set`:
2922
2923```js
2924// the perform method of our trigger
2925// ensure operation.canPaginate is true!
2926
2927const performWithoutAsync = (z, bundle) => {
2928 return Promise.resolve()
2929 .then(() => {
2930 if (bundle.meta.page === 0) {
2931 // first page, no need to fetch a cursor
2932 return Promise.resolve();
2933 } else {
2934 return z.cursor.get(); // Promise<string | null>
2935 }
2936 })
2937 .then(cursor => {
2938 return z.request(
2939 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
2940 {
2941 params: { cursor: cursor } // if cursor is null, it's ignored here
2942 }
2943 );
2944 })
2945 .then(response => {
2946 // need to save the cursor and return a promise, but also need to pass the data along
2947 return Promise.all([response.items, z.cursor.set(response.nextPage)]);
2948 })
2949 .then(([items /* null */]) => {
2950 return items;
2951 });
2952};
2953
2954// ---------------------------------------------------
2955
2956const performWithAsync = async (z, bundle) => {
2957 let cursor;
2958 if (bundle.meta.page) {
2959 cursor = await z.cursor.get(); // string | null
2960 }
2961
2962 const response = await z.request(
2963 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
2964 {
2965 // if cursor is null, it's sent as an empty query
2966 // param and should be ignored by the server
2967 params: { cursor: cursor }
2968 }
2969 );
2970
2971 // we successfully got page 1, should store the cursor in case the user wants page 2
2972 await z.cursor.set(response.nextPage);
2973
2974 return response.items;
2975};
2976
2977```
2978
2979Cursors are stored per-zap and last about an hour. Per the above, make sure to only include the cursor if `bundle.meta.page !== 0`, so you don't accidentally reuse a cursor from a previous poll.
2980
2981Lastly, you need to set `canPaginate` to `true` in your polling definition (per the [schema](https://github.com/zapier/zapier-platform-schema/blob/master/docs/build/schema.md#basicpollingoperationschema)) for the `z.cursor` methods to work as expected.
2982
2983<a id="dedup"></a>
2984### How does deduplication work?
2985
2986Each time a polling Zap runs, Zapier needs to decide which of the items in the response should trigger the zap. To do this, we compare the `id`s to all those we've seen before, trigger on new objects, and update the list of seen `id`s. When a Zap is turned on, we initialize the list of seen `id`s with a single poll. When it's turned off, we clear that list. For this reason, it's important that calls to a polling endpoint always return the newest items.
2987
2988For example, the initial poll returns objects 4, 5, and 6 (where a higher `id` is newer). If a later poll increases the limit and returns objects 1-6, then 1, 2, and 3 will be (incorrectly) treated like new objects.
2989
2990There's a more in-depth explanation [here](https://zapier.com/developer/documentation/v2/deduplication/).
2991
2992### Why are my triggers complaining if I don't provide an explicit `id` field?
2993
2994For deduplication to work, we need to be able to identify and use a unique field. In older, legacy Zapier Web Builder apps, we guessed if `id` wasn't present. In order to ensure we don't guess wrong, we now require that the developers send us an `id` field. If your objects have a differently-named unique field, feel free to adapt this snippet and ensure this test passes:
2995
2996```js
2997// ...
2998let items = z.JSON.parse(response.content).items;
2999items.forEach(item => {
3000 item.id = item.contactId;
3001})
3002
3003return items;
3004```
3005
3006## Command Line Tab Completion
3007
3008We have provided two tab completion scripts to make it easier to use the Zapier Platform CLI, for zsh and bash.
3009
3010### Zsh Completion Script
3011
3012To use the zsh completion script, first setup support for completion, if you haven't already done so. This example assumes your completion scripts are in `~/.zsh/completion`. Adjust accordingly if you put them somewhere else:
3013
3014```zsh
3015# add custom completion scripts
3016fpath=(~/.zsh/completion $fpath)
3017
3018# compsys initialization
3019autoload -U compinit
3020compinit
3021```
3022
3023Next download our completion script to your completions directory:
3024
3025```zsh
3026cd ~/.zsh/completion
3027curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/zsh/_zapier -O
3028```
3029
3030Finally, restart your shell and start hitting TAB with the `zapier` command!
3031
3032### Bash Completion Script
3033
3034To use the bash completion script, first download the completion script. The example assumes your completion scripts are in `~/.bash_completion.d` directory. Adjust accordingly if you put them somewhere else.
3035
3036```bash
3037cd ~/.bash_completion.d
3038curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/bash/_zapier -O
3039```
3040
3041Next source the script from your `~/.bash_profile`:
3042
3043```bash
3044source ~/.bash_completion.d/_zapier
3045```
3046
3047Finally, restart your shell and start hitting TAB with the `zapier` command!
3048
3049## The Zapier Platform Packages
3050
3051The Zapier Platform consists of 3 npm packages that are released simultaneously as a trio.
3052
3053- [`zapier-platform-cli`](https://github.com/zapier/zapier-platform-cli) is the code that powers the `zapier` command. You use it most commonly with the `test`, `scaffold`, and `push` commands. It's installed with `npm install -g zapier-platform-cli` and does not correspond to a particular app.
3054
3055- [`zapier-platform-core`](https://github.com/zapier/zapier-platform-core) is what allows your app to interact with Zapier. It holds the `z` object and app tester code. Your app depends on a specific version of `zapier-platform-core` in the `package.json` file. It's installed via `npm install` along with the rest of your app's dependencies.
3056
3057- [`zapier-platform-schema`](https://github.com/zapier/zapier-platform-schema) enforces app structure behind the scenes. It's a dependency of `core`, so it will be installed automatically.
3058
3059### Updating
3060
3061The Zapier platform and its tools are under active development. While you don't need to install every release, we release new versions because they are better than the last. We do our best to adhere to [Semantic Versioning](https://semver.org/) wherein we won't break your code unless there's a `major` release. Otherwise, we're just fixing bugs (`patch`) and adding features (`minor`).
3062
3063Barring unforseen circumstances, all released platform versions will continue to work for the forseeable future. While you never *have* to upgrade your app's `platform-core` dependency, we recommend keeping an eye on the [changelog](https://github.com/zapier/zapier-platform-cli/blob/master/CHANGELOG.md) to see what new features and bux fixes are available.
3064
3065<!-- TODO: if we decouple releases, change this -->
3066The most recently released version of `cli` and `core` is `8.2.1`. You can see the versions you're working with by running `zapier -v`.
3067
3068To update `cli`, run `npm install -g zapier-platform-cli`.
3069
3070To update the version of `core` your app depends on, set the `zapier-platform-core` dependency in your `package.json` to a version listed [here](https://github.com/zapier/zapier-platform-core/releases) and run `npm install`.
3071
3072For maximum compatibility, keep the versions of `cli` and `core` in sync.
3073
3074## Development of the CLI
3075
3076This section is only relevant if you're editing the `zapier-platform-cli` package itself.
3077
3078### Commands
3079
3080- `export ZAPIER_BASE_ENDPOINT='http://localhost:8001'` if you're building against a local dev environment
3081- `npm install` for getting started
3082- `npm run build` for updating `./lib` from `./src`
3083- `npm run watch` for automatically building as you work
3084- `npm test` for running tests (also runs `npm run build`)
3085- `npm link` (while in this project's directory) and then use `zapier` command elsewhere
3086- `npm run test-convert` for running integration tests for the `zapier convert` command
3087- `npm run docs` for updating docs
3088- `npm run gen-completions` for updating the auto complete scripts
3089
3090### Publishing of the CLI (after merging)
3091
3092- `npm version [patch|minor|major]` will pull, test, update docs, increment version in package.json, push tags, and publish to npm
3093- `npm run validate-templates` for validating the example apps
3094- `npm run set-template-versions VERSION` for updating the platform-core version in the example app repos to `VERSION`
3095
3096## Get Help!
3097
3098You can get help by either emailing partners@zapier.com or by [joining our Slack team here](https://join.slack.com/t/zapier-platform/shared_invite/enQtNTg1MjM5NjMzNTI3LWVhZjk3NjFlNWFiMTBmMDExMzI5NDZiZmFiOTJjYmI3NmY2MzEwZDUxNzQ3MTliMTM5N2M3NGM2MmQ2Y2VkODE).