UNPKG

95.7 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 the CLI Platform Work](#how-does-the-cli-platform-work)
28 * [CLI vs the Web Builder Platform](#cli-vs-the-web-builder-platform)
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 * [Custom](#custom)
44 * [Session](#session)
45 * [OAuth2](#oauth2)
46- [Resources](#resources)
47 * [Resource Definition](#resource-definition)
48- [Triggers/Searches/Creates](#triggerssearchescreates)
49 * [Return Types](#return-types)
50- [Fields](#fields)
51 * [Custom/Dynamic Fields](#customdynamic-fields)
52 * [Dynamic Dropdowns](#dynamic-dropdowns)
53 * [Search-Powered Fields](#search-powered-fields)
54 * [Computed Fields](#computed-fields)
55- [Z Object](#z-object)
56 * [`z.request([url], options)`](#zrequesturl-options)
57 * [`z.console`](#zconsole)
58 * [`z.dehydrate(func, inputData)`](#zdehydratefunc-inputdata)
59 * [`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`](#zstashfilebufferstringstream-knownlength-filename-contenttype)
60 * [`z.JSON`](#zjson)
61 * [`z.hash()`](#zhash)
62 * [`z.errors`](#zerrors)
63 * [`z.cursor`](#zcursor)
64- [Bundle Object](#bundle-object)
65 * [`bundle.authData`](#bundleauthdata)
66 * [`bundle.inputData`](#bundleinputdata)
67 * [`bundle.inputDataRaw`](#bundleinputdataraw)
68 * [`bundle.meta`](#bundlemeta)
69 * [`bundle.rawRequest`](#bundlerawrequest)
70 * [`bundle.cleanedRequest`](#bundlecleanedrequest)
71- [Environment](#environment)
72 * [Defining Environment Variables](#defining-environment-variables)
73 * [Accessing Environment Variables](#accessing-environment-variables)
74- [Making HTTP Requests](#making-http-requests)
75 * [Shorthand HTTP Requests](#shorthand-http-requests)
76 * [Manual HTTP Requests](#manual-http-requests)
77 + [POST and PUT Requests](#post-and-put-requests)
78 * [Using HTTP middleware](#using-http-middleware)
79 * [HTTP Request Options](#http-request-options)
80 * [HTTP Response Object](#http-response-object)
81- [Dehydration](#dehydration)
82- [Stashing Files](#stashing-files)
83- [Logging](#logging)
84 * [Console Logging](#console-logging)
85 * [Viewing Console Logs](#viewing-console-logs)
86 * [Viewing Bundle Logs](#viewing-bundle-logs)
87 * [HTTP Logging](#http-logging)
88 * [Viewing HTTP Logs](#viewing-http-logs)
89- [Error Handling](#error-handling)
90 * [General Errors](#general-errors)
91 * [Halting Execution](#halting-execution)
92 * [Stale Authentication Credentials](#stale-authentication-credentials)
93- [Testing](#testing)
94 * [Writing Unit Tests](#writing-unit-tests)
95 * [Mocking Requests](#mocking-requests)
96 * [Running Unit Tests](#running-unit-tests)
97 * [Testing & Environment Variables](#testing--environment-variables)
98 * [Viewing HTTP Logs in Unit Tests](#viewing-http-logs-in-unit-tests)
99 * [Testing in Your CI](#testing-in-your-ci)
100- [Using `npm` Modules](#using-npm-modules)
101- [Using Transpilers](#using-transpilers)
102- [Example Apps](#example-apps)
103- [FAQs](#faqs)
104 * [Why doesn't Zapier support newer versions of Node.js?](#why-doesnt-zapier-support-newer-versions-of-nodejs)
105 * [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)
106 * [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies)
107 * [Does Zapier support XML (SOAP) APIs?](#does-zapier-support-xml-soap-apis)
108 * [Is it possible to iterate over pages in a polling trigger?](#is-it-possible-to-iterate-over-pages-in-a-polling-trigger)
109 * [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)
110 * [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)
111 * [How does deduplication work?](#how-does-deduplication-work)
112 * [Why are my triggers complaining if I don't provide an explicit `id` field? I didn't have to do that in the Web Builder!](#why-are-my-triggers-complaining-if-i-dont-provide-an-explicit-id-field-i-didnt-have-to-do-that-in-the-web-builder)
113- [Command Line Tab Completion](#command-line-tab-completion)
114 * [Zsh Completion Script](#zsh-completion-script)
115 * [Bash Completion Script](#bash-completion-script)
116- [The Zapier Platform Packages](#the-zapier-platform-packages)
117 * [Updating](#updating)
118- [Development of the CLI](#development-of-the-cli)
119 * [Commands](#commands)
120 * [Publishing of the CLI (after merging)](#publishing-of-the-cli-after-merging)
121- [Get Help!](#get-help)
122
123<!-- tocstop -->
124
125## Getting Started
126
127> 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.
128
129### What is an App?
130
131A CLI App is an implementation of your app's API. You build a Node.js application
132that exports a single object ([JSON Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#appschema)) and upload it to Zapier.
133Zapier introspects that definition to find out what your app is capable of and
134what options to present end users in the Zap Editor.
135
136For those not familiar with Zapier terminology, here is how concepts in the CLI
137map to the end user experience:
138
139 * [Authentication](#authentication), (usually) which lets us know what credentials to ask users
140 for. This is used during the "Connect Accounts" section of the Zap Editor.
141 * [Triggers](#triggerssearchescreates), which read data *from* your API. These have their own section in the Zap Editor.
142 * [Creates](#triggerssearchescreates), which send data *to* your API to create new records. These are listed under "Actions" in the Zap Editor.
143 * [Searches](#triggerssearchescreates), which find specific records *in* your system. These are also listed under "Actions" in the Zap Editor.
144 * [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.
145
146### How does the CLI Platform Work
147
148Zapier takes the App you upload and sends it over to Amazon Web Service's Lambda.
149We then make calls to execute the operations your App defines as we execute Zaps.
150Your App takes the input data we provide (if any), makes the necessary HTTP calls,
151and returns the relevant data, which gets fed back into Zapier.
152
153### CLI vs the Web Builder Platform
154
155From a user perspective, both the CLI and the existing web builder platform offer the same experience. The biggest difference is how they're developed. The CLI takes a much more code-first approach, allowing you to develop your Zapier app just like you would any other programming project. The web builder, on the other hand, is much better for folks who want to make an app with minimal coding involved. Both will continue to coexist, so pick whichever fits your needs best!
156
157### Requirements
158
159All Zapier CLI apps are run using Node.js `v8.10.0`.
160
161You 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).
162
163To 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.
164
165Firstly, 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.
166
167Alternatively, 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).
168Then 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.
169
170
171### Quick Setup Guide
172
173First 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).
174
175```bash
176# install the CLI globally
177npm install -g zapier-platform-cli
178
179# setup auth to Zapier's platform with a deploy key
180zapier login
181```
182
183Your Zapier CLI should be installed and ready to go at this point. Next up, we'll create our first app!
184
185```bash
186# create a directory with the minimum required files
187zapier init example-app
188
189# move into the new directory
190cd example-app
191
192# install all the libraries needed for your app
193npm install
194```
195
196> Note: there are plenty of templates & example apps to choose from! [View all Example Apps here.](#example-apps)
197
198You should now have a working local app. You can run several local commands to try it out.
199
200```bash
201# run the local tests
202# the same as npm test, but adds some extra things to the environment
203zapier test
204```
205
206Next, you'll probably want to upload app to Zapier itself so you can start testing live.
207
208```bash
209# push your app to Zapier
210zapier push
211```
212
213> Go check out our [full CLI reference documentation](http://zapier.github.io/zapier-platform-cli/cli.html) to see all the other commands!
214
215
216### Tutorial
217
218For 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!
219
220## Creating a Local App
221
222> Tip: check the [Quick Setup](#quick-setup-guide) if this is your first time using the platform!
223
224Creating 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`.
225
226```bash
227# make your folder
228mkdir zapier-example
229cd zapier-example
230
231# create the needed files from a template
232zapier init . --template=trigger
233
234# install all the libraries needed for your app
235npm install
236```
237
238If you'd like to manage your **local App**, use these commands:
239
240* `zapier init . --template=resource` - initialize/start a local app project ([see templates here](https://github.com/zapier/zapier-platform-cli/wiki/Example-Apps))
241* `zapier convert 1234 .` - initialize/start from an existing app (alpha)
242* `zapier scaffold resource Contact` - auto-injects a new resource, trigger, etc.
243* `zapier test` - run the same tests as `npm test`
244* `zapier validate` - ensure your app is valid
245* `zapier describe` - print some helpful information about your app
246
247### Local Project Structure
248
249In 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.
250
251```plain
252$ tree .
253.
254├── README.md
255├── index.js
256├── package.json
257├── triggers
258│   └── contact-by-tag.js
259├── resources
260│   └── Contact.js
261├── test
262│   ├── basic.js
263│   ├── triggers.js
264│   └── resources.js
265├── build
266│   └── build.zip
267└── node_modules
268 ├── ...
269 └── ...
270```
271
272### Local App Definition
273
274The core definition of your `App` will look something like this, and is what your `index.js` should provide as the _only_ export:
275
276```js
277const App = {
278 // both version strings are required
279 version: require('./package.json').version,
280 platformVersion: require('zapier-platform-core').version,
281
282 // see "Authentication" section below
283 authentication: {},
284
285 // see "Dehydration" section below
286 hydrators: {},
287
288 // see "Making HTTP Requests" section below
289 requestTemplate: {},
290 beforeRequest: [],
291 afterResponse: [],
292
293 // See "Resources" section below
294 resources: {},
295
296 // See "Triggers/Searches/Creates" section below
297 triggers: {},
298 searches: {},
299 creates: {}
300};
301
302module.exports = App;
303
304```
305
306> Tip: you can use higher order functions to create any part of your App definition!
307
308
309## Registering an App
310
311Registering 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.
312
313```bash
314# register your app
315zapier register "Zapier Example"
316
317# list your apps
318zapier apps
319```
320
321> Note: this doesn't put your app in the editor - see the docs on pushing an App Version to do that!
322
323If you'd like to manage your **App**, use these commands:
324
325* `zapier apps` - list the apps in Zapier you can administer
326* `zapier register "Name"` - creates a new app in Zapier
327* `zapier link` - lists and links a selected app in Zapier to your current folder
328* `zapier history` - print the history of your app
329* `zapier collaborate [user@example.com]` - add admins to your app who can push
330* `zapier invite [user@example.com] [1.0.0]` - add users to try your app version 1.0.0 before promotion
331
332
333## Deploying an App Version
334
335An 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. By default, **every App Version is private** but you can `zapier promote` it to production for use by over 1 million Zapier users.
336
337```bash
338# push your app version to Zapier
339zapier push
340
341# list your versions
342zapier versions
343```
344
345If you'd like to manage your **Version**, use these commands:
346
347* `zapier versions` - list the versions for the current directory's app
348* `zapier push` - push the current version of current directory's app & version (read from `package.json`)
349* `zapier promote [1.0.0]` - mark a version as the "production" version
350* `zapier migrate [1.0.0] [1.0.1] [100%]` - move users between versions, regardless of deployment status
351* `zapier deprecate [1.0.0] [YYYY-MM-DD]` - mark a version as deprecated, but let users continue to use it (we'll email them)
352* `zapier env 1.0.0 [KEY] [value]` - set an environment variable to some value
353
354> Note: To see the changes that were just pushed reflected in the browser, you have to manually refresh the browser each time you push.
355
356
357### Private App Version (default)
358
359A simple `zapier push` will only create the App Version in your editor. No one else using Zapier can see it or use it.
360
361
362### Sharing an App Version
363
364This 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.
365
366```bash
367# sends an email this user to let them view the app version 1.0.0 in the UI privately
368zapier invite user@example.com 1.0.0
369
370# sends an email this user to let them admin the app (make changes just like you)
371zapier collaborate user@example.com
372```
373
374You 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.
375
376
377### Promoting an App Version
378
379Promotion 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.
380
381If 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.
382
383```bash
384# promote your app version to all Zapier users
385zapier promote 1.0.1
386
387# OPTIONAL - migrate your users between one app version to another
388zapier migrate 1.0.0 1.0.1
389
390# OR - mark the old version as deprecated
391zapier deprecate 1.0.0 2017-01-01
392```
393
394## Converting an Existing App
395
396If you have an existing Web Builder app on [Zapier Developer Platform](https://zapier.com/developer/builder/) you can use it as a template to kickstart your local application.
397
398```bash
399# Convert an existing Web Builder app to a CLI app in the my-app directory
400# App ID 1234 is from URL https://zapier.com/developer/builder/app/1234/development
401zapier convert 1234 my-app
402```
403
404Your CLI app will be created and you can continue working on it.
405
406> 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.
407
408> Note - there is no way to convert a CLI app to a Web Builder app and we do not plan on implementing this.
409
410## Authentication
411
412Most 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.
413
414> 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.
415
416### Basic
417
418Useful 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).
419
420> Example App: check out https://github.com/zapier/zapier-platform-example-app-basic-auth for a working example app for basic auth.
421
422> Note: if you do the common API Key pattern like `Authorization: Basic APIKEYHERE:x` you should look at the "Custom" authentication method instead.
423
424```js
425const authentication = {
426 type: 'basic',
427 // "test" could also be a function
428 test: {
429 url: 'https://example.com/api/accounts/me.json'
430 },
431 connectionLabel: '{{bundle.authData.username}}' // Can also be a function, check digest auth below for an example
432 // you can provide additional fields, but we'll provide `username`/`password` automatically
433};
434
435const App = {
436 // ...
437 authentication: authentication
438 // ...
439};
440
441```
442
443### Custom
444
445This 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.
446
447> Example App: check out https://github.com/zapier/zapier-platform-example-app-custom-auth for a working example app for custom auth.
448
449```js
450const authentication = {
451 type: 'custom',
452 // "test" could also be a function
453 test: {
454 url:
455 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json'
456 },
457 fields: [
458 {
459 key: 'subdomain',
460 type: 'string',
461 required: true,
462 helpText: 'Found in your browsers address bar after logging in.'
463 },
464 {
465 key: 'api_key',
466 type: 'string',
467 required: true,
468 helpText: 'Found on your settings page.'
469 }
470 ]
471};
472
473const addApiKeyToHeader = (request, z, bundle) => {
474 request.headers['X-Subdomain'] = bundle.authData.subdomain;
475 const basicHash = Buffer(`${bundle.authData.api_key}:x`).toString('base64');
476 request.headers.Authorization = `Basic ${basicHash}`;
477 return request;
478};
479
480const App = {
481 // ...
482 authentication: authentication,
483 beforeRequest: [addApiKeyToHeader]
484 // ...
485};
486
487```
488
489### Session
490
491Probably 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).
492
493> Example App: check out https://github.com/zapier/zapier-platform-example-app-session-auth for a working example app for session auth.
494
495```js
496const getSessionKey = (z, bundle) => {
497 const promise = z.request({
498 method: 'POST',
499 url: 'https://example.com/api/accounts/login.json',
500 body: {
501 username: bundle.authData.username,
502 password: bundle.authData.password
503 }
504 });
505
506 return promise.then(response => {
507 if (response.status === 401) {
508 throw new Error('The username/password you supplied is invalid');
509 }
510 return {
511 sessionKey: z.JSON.parse(response.content).sessionKey
512 };
513 });
514};
515
516const authentication = {
517 type: 'session',
518 // "test" could also be a function
519 test: {
520 url: 'https://example.com/api/accounts/me.json'
521 },
522 fields: [
523 {
524 key: 'username',
525 type: 'string',
526 required: true,
527 helpText: 'Your login username.'
528 },
529 {
530 key: 'password',
531 type: 'string',
532 required: true,
533 helpText: 'Your login password.'
534 }
535 // For Session Auth we store `sessionKey` automatically in `bundle.authData`
536 // for future use. If you need to save/use something that the user shouldn't
537 // need to type/choose, add a "computed" field, like:
538 // {key: 'something': type: 'string', required: false, computed: true}
539 // And remember to return it in sessionConfig.perform
540 ],
541 sessionConfig: {
542 perform: getSessionKey
543 }
544};
545
546const includeSessionKeyHeader = (request, z, bundle) => {
547 if (bundle.authData.sessionKey) {
548 request.headers = request.headers || {};
549 request.headers['X-Session-Key'] = bundle.authData.sessionKey;
550 }
551 return request;
552};
553
554const sessionRefreshIf401 = (response, z, bundle) => {
555 if (bundle.authData.sessionKey) {
556 if (response.status === 401) {
557 throw new z.errors.RefreshAuthError(); // ask for a refresh & retry
558 }
559 }
560 return response;
561};
562
563const App = {
564 // ...
565 authentication: authentication,
566 beforeRequest: [includeSessionKeyHeader],
567 afterResponse: [sessionRefreshIf401]
568 // ...
569};
570
571```
572
573> 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.
574
575### OAuth2
576
577Zapier'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/).
578
579> Example App: check out https://github.com/zapier/zapier-platform-example-app-oauth2 for a working example app for oauth2.
580
581It looks like this:
582
583 1. Zapier sends the user to the authorization URL defined by your App
584 2. Once authorized, your website sends the user to the `redirect_uri` Zapier provided (`zapier describe` to find out what it is)
585 3. Zapier makes a call on the backend to your API to exchange the `code` for an `access_token`
586 4. Zapier remembers the `access_token` and makes calls on behalf of the user
587 5. (Optionally) Zapier can refresh the token if it expires
588
589You 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:
590
591```bash
592# setting the environment variables on Zapier.com
593$ zapier env 1.0.0 CLIENT_ID 1234
594$ zapier env 1.0.0 CLIENT_SECRET abcd
595
596# and when running tests locally, don't forget to define them!
597$ CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
598```
599
600Your auth definition would look something like this:
601
602```js
603const authentication = {
604 type: 'oauth2',
605 test: {
606 url:
607 'https://{{bundle.authData.subdomain}}.example.com/api/accounts/me.json'
608 },
609 // you can provide additional fields for inclusion in authData
610 oauth2Config: {
611 // "authorizeUrl" could also be a function returning a string url
612 authorizeUrl: {
613 method: 'GET',
614 url:
615 'https://{{bundle.inputData.subdomain}}.example.com/api/oauth2/authorize',
616 params: {
617 client_id: '{{process.env.CLIENT_ID}}',
618 state: '{{bundle.inputData.state}}',
619 redirect_uri: '{{bundle.inputData.redirect_uri}}',
620 response_type: 'code'
621 }
622 },
623 // Zapier expects a response providing {access_token: 'abcd'}
624 // "getAccessToken" could also be a function returning an object
625 getAccessToken: {
626 method: 'POST',
627 url:
628 'https://{{bundle.inputData.subdomain}}.example.com/api/v2/oauth2/token',
629 body: {
630 code: '{{bundle.inputData.code}}',
631 client_id: '{{process.env.CLIENT_ID}}',
632 client_secret: '{{process.env.CLIENT_SECRET}}',
633 redirect_uri: '{{bundle.inputData.redirect_uri}}',
634 grant_type: 'authorization_code'
635 },
636 headers: {
637 'Content-Type': 'application/x-www-form-urlencoded'
638 }
639 },
640 scope: 'read,write'
641 },
642 // If you need any fields upfront, put them here
643 fields: [
644 { key: 'subdomain', type: 'string', required: true, default: 'app' }
645 // For OAuth we store `access_token` and `refresh_token` automatically
646 // in `bundle.authData` for future use. If you need to save/use something
647 // that the user shouldn't need to type/choose, add a "computed" field, like:
648 // {key: 'something': type: 'string', required: false, computed: true}
649 // And remember to return it in oauth2Config.getAccessToken/refreshAccessToken
650 ]
651};
652
653const addBearerHeader = (request, z, bundle) => {
654 if (bundle.authData && bundle.authData.access_token) {
655 request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
656 }
657 return request;
658};
659
660const App = {
661 // ...
662 authentication: authentication,
663 beforeRequest: [addBearerHeader]
664 // ...
665};
666
667module.exports = App;
668
669```
670
671> Note - For OAuth, `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 the first time the Zap runs. 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)
672
673
674## Resources
675
676A `resource` is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a `/recipes`
677endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
678read, and search operations on that resource.
679
680```js
681const Recipe = {
682 // `key` is the unique identifier the Zapier backend references
683 key: 'recipe',
684 // `noun` is the user-friendly name displayed in the Zapier UI
685 noun: 'Recipe',
686 // `list` and `create` are just a couple of the methods you can define
687 list: {
688 //...
689 },
690 create: {
691 //...
692 }
693};
694
695```
696
697The quickest way to create a resource is with the `zapier scaffold` command:
698
699```bash
700zapier scaffold resource "Recipe"
701```
702
703This will generate the resource file and add the necessary statements to the `index.js` file to import it.
704
705
706### Resource Definition
707
708A resource has a few basic properties. The first is the `key`, which allows Zapier to identify the resource on our backend.
709The second is the `noun`, the user-friendly name of the resource that is presented to users throughout the Zapier UI.
710
711> Example App: check out https://github.com/zapier/zapier-platform-example-app-resource for a working example app using resources.
712
713After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource.
714The complete list of available methods can be found in the [Resource Schema Docs](https://zapier.github.io/zapier-platform-schema/build/schema.html#resourceschema).
715For now, let's focus on two:
716
717 * `list` - Tells Zapier how to fetch a set of this resource. This becomes a Trigger in the Zapier Editor.
718 * `create` - Tells Zapier how to create a new instance of the resource. This becomes an Action in the Zapier Editor.
719
720Here is a complete example of what the list method might look like
721
722```js
723const listRecipesRequest = {
724 url: 'http://example.com/recipes'
725};
726
727const Recipe = {
728 key: 'recipe',
729 //...
730 list: {
731 display: {
732 label: 'New Recipe',
733 description: 'Triggers when a new recipe is added.'
734 },
735 operation: {
736 perform: listRecipesRequest
737 }
738 }
739};
740
741```
742
743The 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.
744
745Adding a create method looks very similar.
746
747```js
748const createRecipeRequest = {
749 url: 'http://example.com/recipes',
750 method: 'POST',
751 body: {
752 name: 'Baked Falafel',
753 style: 'mediterranean'
754 }
755};
756
757const Recipe = {
758 key: 'recipe',
759 //...
760 list: {
761 //...
762 },
763 create: {
764 display: {
765 label: 'Add Recipe',
766 description: 'Adds a new recipe to our cookbook.'
767 },
768 operation: {
769 perform: createRecipeRequest
770 }
771 }
772};
773
774```
775
776Every method you define on a `resource` Zapier converts to the appropriate Trigger, Create, or Search. Our examples
777above would result in an app with a New Recipe Trigger and an Add Recipe Create.
778
779Note 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`.
780
781
782## Triggers/Searches/Creates
783
784Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read
785data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create
786new records in your system (add a recipe to the catalog).
787
788The definition for each of these follows the same structure. Here is an example of a trigger:
789
790```js
791const recipeListRequest = {
792 url: 'http://example.com/recipes'
793};
794
795const App = {
796 //...
797 triggers: {
798 new_recipe: {
799 key: 'new_recipe', // uniquely identifies the trigger
800 noun: 'Recipe', // user-friendly word that is used to refer to the resource
801 // `display` controls the presentation in the Zapier Editor
802 display: {
803 label: 'New Recipe',
804 description: 'Triggers when a new recipe is added.'
805 },
806 // `operation` implements the API call used to fetch the data
807 operation: {
808 perform: recipeListRequest
809 }
810 },
811 another_trigger: {
812 // Another trigger definition...
813 }
814 }
815};
816
817```
818
819You 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),
820[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).
821
822> Example App: check out https://github.com/zapier/zapier-platform-example-app-trigger for a working example app using triggers.
823
824> Example App: check out https://github.com/zapier/zapier-platform-example-app-rest-hooks for a working example app using REST hook triggers.
825
826> Example App: check out https://github.com/zapier/zapier-platform-example-app-search for a working example app using searches.
827
828> Example App: check out https://github.com/zapier/zapier-platform-example-app-create for a working example app using creates.
829
830### Return Types
831
832Each 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:
833
834| Method | Return Type | Notes |
835| --- | --- | --- |
836| Trigger | Array | 0 or more objects that will be passed to the [deduper](https://zapier.com/developer/documentation/v2/deduplication/) |
837| Search | Array | 0 or more objects. If len > 0, put the best match first |
838| Action | Object | Return values are evaluated by [`isPlainObject`](https://lodash.com/docs#isPlainObject) |
839
840## Fields
841
842On each trigger, search, or create in the `operation` directive - you can provide an array of objects as fields under the `inputFields`. 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.
843
844You can find more details on each and every field option at [Field Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#fieldschema).
845
846Those fields have various options you can provide, here is a succinct example:
847
848```js
849const App = {
850 //...
851 creates: {
852 create_recipe: {
853 //...
854 operation: {
855 // an array of objects is the simplest way
856 inputFields: [
857 {
858 key: 'title',
859 required: true,
860 label: 'Title of Recipe',
861 helpText: 'Name your recipe!'
862 },
863 {
864 key: 'style',
865 required: true,
866 choices: { mexican: 'Mexican', italian: 'Italian' }
867 }
868 ],
869 perform: () => {}
870 }
871 }
872 }
873};
874
875```
876
877### Custom/Dynamic Fields
878
879In 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.
880
881> 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).
882
883> A function that returns a list of dynamic fields cannot include additional functions in that list to call for dynamic fields.
884
885```js
886const recipeFields = (z, bundle) => {
887 const response = z.request('http://example.com/api/v2/fields.json');
888 // json is is [{"key":"field_1"},{"key":"field_2"}]
889 return response.then(res => res.json);
890};
891
892const App = {
893 //...
894 creates: {
895 create_recipe: {
896 //...
897 operation: {
898 // an array of objects is the simplest way
899 inputFields: [
900 {
901 key: 'title',
902 required: true,
903 label: 'Title of Recipe',
904 helpText: 'Name your recipe!'
905 },
906 {
907 key: 'style',
908 required: true,
909 choices: { mexican: 'Mexican', italian: 'Italian' }
910 },
911 recipeFields // provide a function inline - we'll merge the results!
912 ],
913 perform: () => {}
914 }
915 }
916 }
917};
918
919```
920
921Additionally, 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."
922
923```js
924module.exports = {
925 key: 'dessert',
926 noun: 'Dessert',
927 display: {
928 label: 'Order Dessert',
929 description: 'Orders a dessert.'
930 },
931 operation: {
932 inputFields: [
933 {
934 key: 'type',
935 required: true,
936 choices: { 1: 'cake', 2: 'ice cream', 3: 'cookie' },
937 altersDynamicFields: true
938 },
939 function(z, bundle) {
940 if (bundle.inputData.type === '2') {
941 return [{ key: 'with_sprinkles', type: 'boolean' }];
942 }
943 return [];
944 }
945 ],
946 perform: function(z, bundle) {
947 /* ... */
948 }
949 }
950};
951
952```
953
954> Only dropdowns support `altersDynamicFields`.
955
956### Dynamic Dropdowns
957
958Sometimes, API endpoints require clients to specify a parent object in order to create or access the child resources. Imagine having to specify a company id in order to get a list of employees for that company. 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.
959
960Our 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."
961
962To define one, you can provide the `dynamic` property on your field to specify the trigger that should be used to populate the options for the dropdown. The value for the property is a dot-separated concatenation of a trigger's key, the field to use for the value, and the field to use for the label.
963
964```js
965const App = {
966 //...
967 resources: {
968 project: {
969 key: 'project',
970 //...
971 list: {
972 //...
973 operation: {
974 perform: () => {
975 return [{ id: 123, name: 'Project 1' }];
976 } // called for project_id dropdown
977 }
978 }
979 },
980 issue: {
981 key: 'issue',
982 //...
983 create: {
984 //...
985 operation: {
986 inputFields: [
987 {
988 key: 'project_id',
989 required: true,
990 label: 'Project',
991 dynamic: 'projectList.id.name'
992 }, // calls project.list
993 {
994 key: 'title',
995 required: true,
996 label: 'Title',
997 helpText: 'What is the name of the issue?'
998 }
999 ]
1000 }
1001 }
1002 }
1003 }
1004};
1005
1006```
1007
1008In the UI, users will see something like this:
1009
1010![screenshot of dynamic dropdown in Zap Editor](https://cdn.zapier.com/storage/photos/dd31fa761e0cf9d0abc9b50438f95210.png)
1011
1012> Dynamic dropdowns are one of the few fields that automatically invalidate Zapier's field cache, so it is not necessary to set `altersDynamicFields` to true for these fields.
1013
1014### Search-Powered Fields
1015
1016For 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.
1017
1018```js
1019const App = {
1020 //...
1021 resources: {
1022 project: {
1023 key: 'project',
1024 //...
1025 search: {
1026 //...
1027 operation: {
1028 perform: () => {
1029 return [{ id: 123, name: 'Project 1' }];
1030 } // called for project_id
1031 }
1032 }
1033 },
1034 issue: {
1035 key: 'issue',
1036 //...
1037 create: {
1038 //...
1039 operation: {
1040 inputFields: [
1041 {
1042 key: 'project_id',
1043 required: true,
1044 label: 'Project',
1045 dynamic: 'projectList.id.name',
1046 search: 'projectSearch.id'
1047 }, // calls project.search (requires a trigger in the "dynamic" property)
1048 {
1049 key: 'title',
1050 required: true,
1051 label: 'Title',
1052 helpText: 'What is the name of the issue?'
1053 }
1054 ]
1055 }
1056 }
1057 }
1058 }
1059};
1060
1061```
1062
1063**NOTE:** This has to be combined with the `dynamic` property to give the user a guided experience when setting up a Zap.
1064
1065If you don't define a trigger for the `dynamic` property, the search connector won't show.
1066
1067### Computed Fields
1068
1069In OAuth and Session Auth, you might want to store fields in `bundle.authData` (other than `access_token`, `refresh_token` — for OAuth —, or `sessionKey` — for Session Auth), that you don't want the user to fill in.
1070
1071For those situations, you need a computed field. It's just like another field, but with a `computed: true` property (don't forget to also make it `required: false`). You can see examples in the [OAuth](#oauth2) or [Session Auth](#session) example sections.
1072
1073## Z Object
1074
1075We provide several methods off of the `z` object, which is provided as the first argument to all function calls in your app.
1076
1077> The `z` object is passed into your functions as the first argument - IE: `perform: (z) => {}`.
1078
1079### `z.request([url], options)`
1080
1081`z.request([url], options)` is a promise based HTTP client with some Zapier-specific goodies. See [Making HTTP Requests](#making-http-requests).
1082
1083### `z.console`
1084
1085`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)
1086
1087### `z.dehydrate(func, inputData)`
1088
1089`z.dehydrate(func, inputData)` is used to lazily evaluate a function, perfect to avoid API calls during polling or for reuse. See [Dehydration](#dehydration).
1090
1091### `z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])`
1092
1093`z.stashFile(bufferStringStream, [knownLength], [filename], [contentType])` is a promise based file stasher that returns a URL file pointer. See [Stashing Files](#stashing-files).
1094
1095### `z.JSON`
1096
1097`z.JSON` is similar to the JSON built-in like `z.JSON.parse('...')`, but catches errors and produces nicer tracebacks.
1098
1099### `z.hash()`
1100
1101`z.hash()` is a crypto tool for doing things like `z.hash('sha256', 'my password')`
1102
1103### `z.errors`
1104
1105`z.errors` is a collection error classes that you can throw in your code, like `throw new z.errors.HaltedError('...')`.
1106
1107The available errors are:
1108
1109* HaltedError - Stops current operation, but will never turn off Zap. Read more on [Halting Execution](#halting-execution)
1110* ExpiredAuthError - Turns off Zap and emails user to manually reconnect. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1111* RefreshAuthError - (OAuth2 or Session Auth) Tells Zapier to refresh credentials and retry operation. Read more on [Stale Authentication Credentials](#stale-authentication-credentials)
1112
1113
1114For more details on error handling in general, see [here](#error-handling).
1115
1116### `z.cursor`
1117
1118The `z.cursor` object exposes two methods:
1119
1120* `z.cursor.get(): Promise<string|null>`
1121* `z.cursor.set(string): Promise<null>`
1122
1123Any data you `set` will be available to that Zap for about an hour (or until it's overwritten). For more information, see: [paging](#paging).
1124
1125## Bundle Object
1126
1127This object holds the user's auth details and the data for the API requests.
1128
1129> The `bundle` object is passed into your functions as the second argument - IE: `perform: (z, bundle) => {}`.
1130
1131### `bundle.authData`
1132
1133`bundle.authData` is user-provided authentication data, like `api_key` or `access_token`. [Read more on authentication.](#authentication)
1134
1135### `bundle.inputData`
1136
1137`bundle.inputData` is user-provided data for this particular run of the trigger/search/create, as defined by the inputFields. For example:
1138
1139```js
1140{
1141 createdBy: 'his name is Bobby Flay'
1142 style: 'he cooks mediterranean'
1143}
1144```
1145
1146### `bundle.inputDataRaw`
1147
1148`bundle.inputDataRaw` is kind of like `inputData`, but before rendering `{{curlies}}`:
1149
1150```js
1151{
1152 createdBy: 'his name is {{123__chef_name}}'
1153 style: 'he cooks {{456__style}}'
1154}
1155```
1156
1157> "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.
1158
1159### `bundle.meta`
1160
1161`bundle.meta` is extra information useful for doing advanced behaviors depending on what the user is doing. It has the following options:
1162
1163| key | default | description |
1164| --- | --- | --- |
1165| frontend | `false` | if true, this run was initiated manually via the Zap editor |
1166| prefill | `false` | if true, this poll is being used to populate a dynamic dropdown |
1167| hydrate | `true` | if true, the results of this run will be hydrated (false if we're in the middle of hydrating already) |
1168| test_poll | `false` | if true, the poll was triggered by a user testing their account (via [clicking "test"](https://cdn.zapier.com/storage/photos/5c94c304ce11b02c073a973466a7b846.png) on the auth |
1169| standard_poll| `true` | the opposite of `test_poll` |
1170| first_poll | `false` | if true, the results of this poll will be used to initialize the deduplication list rather than trigger a zap. See: [deduplication](#dedup) |
1171| limit | `-1` | the number of items to fetch. `-1` indicates there's no limit (which will almost always be the case) |
1172| page | `0` | used in [paging](#paging) to uniquely identify which page of results should be returned |
1173
1174> `bundle.meta.zap.id` is only available in the `performSubscribe` and `performUnsubscribe` methods
1175
1176The user's Zap ID is available during the [subscribe and unsubscribe](https://zapier.github.io/zapier-platform-schema/build/schema.html#basichookoperationschema) methods.
1177
1178For example - you could do:
1179
1180```js
1181const subscribeHook = (z, bundle) => {
1182
1183 const options = {
1184 url: 'http://57b20fb546b57d1100a3c405.mockapi.io/api/hooks',
1185 method: 'POST',
1186 body: {
1187 url: bundle.targetUrl, // bundle.targetUrl has the Hook URL this app should call
1188 zap_id: bundle.meta.zap.id,
1189 },
1190 };
1191
1192 return z.request(options).then((response) => response.json);
1193};
1194
1195module.exports = {
1196 // ... see our rest hook example for additional details: https://github.com/zapier/zapier-platform-example-app-rest-hooks/blob/master/triggers/recipe.js
1197 performSubscribe: subscribeHook,
1198 // ...
1199};
1200```
1201
1202### `bundle.rawRequest`
1203> `bundle.rawRequest` is only available in the `perform` for web hooks and `getAccessToken` for oauth authentication methods
1204
1205`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:
1206
1207```
1208{
1209 method: 'POST',
1210 querystring: 'foo=bar&baz=qux',
1211 headers: {
1212 'Content-Type': 'application/json'
1213 },
1214 content: '{"hello": "world"}'
1215}
1216```
1217
1218
1219
1220### `bundle.cleanedRequest`
1221> `bundle.cleanedRequest` is only available in the `perform` for web hooks and `getAccessToken` for oauth authentication methods
1222
1223`bundle.cleanedRequest` will return a formatted and parsed version of the request. Some or all of the following will be available:
1224
1225```
1226{
1227 method: 'POST',
1228 querystring: {
1229 foo: 'bar',
1230 baz: 'qux'
1231 },
1232 headers: {
1233 'Content-Type': 'application/json'
1234 },
1235 content: {
1236 hello: 'world'
1237 }
1238}
1239```
1240
1241
1242## Environment
1243
1244Apps can define environment variables that are available when the app's code executes. They work just like environment
1245variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you
1246don't want to commit to source control. Environment variables can also be used as a quick way to toggle between a
1247a staging and production environment during app development.
1248
1249It is important to note that **variables are defined on a per-version basis!** When you push a new version, the
1250existing variables from the previous version are copied, so you don't have to manually add them. However, edits
1251made to one version's environment will not affect the other versions.
1252
1253### Defining Environment Variables
1254
1255To define an environment variable, use the `env` command:
1256
1257```bash
1258# Will set the environment variable on Zapier.com
1259zapier env 1.0.0 MY_SECRET_VALUE 1234
1260```
1261
1262You will likely also want to set the value locally for testing.
1263
1264```bash
1265export MY_SECRET_VALUE=1234
1266```
1267
1268Alternatively, we provide some extra tooling to work with an `.env` (or `.environment`, see below note) that looks like this:
1269
1270```
1271MY_SECRET_VALUE=1234
1272```
1273
1274> `.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.
1275
1276And then in your `test/basic.js` file:
1277
1278```js
1279const zapier = require('zapier-platform-core');
1280
1281should('some tests', () => {
1282 zapier.tools.env.inject(); // testing only!
1283 console.log(process.env.MY_SECRET_VALUE);
1284 // should print '1234'
1285});
1286```
1287
1288> This is a popular way to provide `process.env.ACCESS_TOKEN || bundle.authData.access_token` for convenient testing.
1289
1290> **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`.
1291
1292
1293### Accessing Environment Variables
1294
1295To view existing environment variables, use the `env` command.
1296
1297```bash
1298# Will print a table listing the variables for this version
1299zapier env 1.0.0
1300```
1301
1302Within your app, you can access the environment via the standard `process.env` - any values set via local `export` or `zapier env` will be there.
1303
1304For example, you can access the `process.env` in your perform functions and in templates:
1305
1306```js
1307const listExample = (z, bundle) => {
1308 const httpOptions = {
1309 headers: {
1310 'my-header': process.env.MY_SECRET_VALUE
1311 }
1312 };
1313 const response = z.request(
1314 'http://example.com/api/v2/recipes.json',
1315 httpOptions
1316 );
1317 return response.then(res => res.json);
1318};
1319
1320const App = {
1321 // ...
1322 triggers: {
1323 example: {
1324 noun: '{{process.env.MY_NOUN}}',
1325 operation: {
1326 // ...
1327 perform: listExample
1328 }
1329 }
1330 }
1331};
1332
1333```
1334
1335> Note! Be sure to lazily access your environment variables - see [When to use placeholders or curlies?](#when-to-use-placeholders-or-curlies)
1336
1337
1338## Making HTTP Requests
1339
1340There are two primary ways to make HTTP requests in the Zapier platform:
1341
13421. **Shorthand HTTP Requests** - these are simple object literals that make it easy to define simple requests.
13432. **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).
1344
1345There are also a few helper constructs you can use to reduce boilerplate:
1346
13471. `requestTemplate` which is an shorthand HTTP request that will be merged with every request.
13482. `beforeRequest` middleware which is an array of functions to mutate a request before it is sent.
13493. `afterResponse` middleware which is an array of functions to mutate a response before it is completed.
1350
1351> Note: you can install any HTTP client you like - but this is greatly discouraged as you lose [automatic HTTP logging](#http-logging) and middleware.
1352
1353### Shorthand HTTP Requests
1354
1355For 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.
1356
1357This features:
1358
13591. Lazy `{{curly}}` replacement.
13602. JSON de-serialization.
13613. Automatic non-2xx error raising.
1362
1363```js
1364const triggerShorthandRequest = {
1365 method: 'GET',
1366 url: 'http://{{bundle.authData.subdomain}}.example.com/v2/api/recipes.json',
1367 params: {
1368 sort_by: 'id',
1369 sort_order: 'DESC'
1370 }
1371};
1372
1373const App = {
1374 // ...
1375 triggers: {
1376 example: {
1377 // ...
1378 operation: {
1379 // ...
1380 perform: triggerShorthandRequest
1381 }
1382 }
1383 }
1384};
1385
1386```
1387
1388In 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.
1389
1390An error will be raised if the response is not valid JSON, so _do not use shorthand HTTP requests with non-JSON responses_.
1391
1392### Manual HTTP Requests
1393
1394When 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.
1395
1396To make a manual HTTP request, use the `request` method of the `z` object:
1397
1398```js
1399const listExample = (z, bundle) => {
1400 const customHttpOptions = {
1401 headers: {
1402 'my-header': 'from zapier'
1403 }
1404 };
1405
1406 return z
1407 .request('http://example.com/api/v2/recipes.json', customHttpOptions)
1408 .then(response => {
1409 if (response.status >= 300) {
1410 throw new Error(`Unexpected status code ${response.status}`);
1411 }
1412
1413 const recipes = z.JSON.parse(response.content);
1414 // do any custom processing of recipes here...
1415
1416 return recipes;
1417 });
1418};
1419
1420const App = {
1421 // ...
1422 triggers: {
1423 example: {
1424 // ...
1425 operation: {
1426 // ...
1427 perform: listExample
1428 }
1429 }
1430 }
1431};
1432
1433```
1434
1435#### POST and PUT Requests
1436
1437To POST or PUT data to your API you can do this:
1438
1439```js
1440const App = {
1441 // ...
1442 triggers: {
1443 example: {
1444 // ...
1445 operation: {
1446 // ...
1447 perform: (z, bundle) => {
1448 const recipe = {
1449 name: 'Baked Falafel',
1450 style: 'mediterranean',
1451 directions: 'Get some dough....'
1452 };
1453
1454 const options = {
1455 method: 'POST',
1456 body: JSON.stringify(recipe)
1457 };
1458
1459 return z
1460 .request('http://example.com/api/v2/recipes.json', options)
1461 .then(response => {
1462 if (response.status !== 201) {
1463 throw new Error(`Unexpected status code ${response.status}`);
1464 }
1465 });
1466 }
1467 }
1468 }
1469 }
1470};
1471
1472```
1473
1474> Note: you need to call `z.JSON.stringify()` before setting the `body`.
1475
1476### Using HTTP middleware
1477
1478If you need to process all HTTP requests in a certain way, you may be able to use one of utility HTTP middleware functions.
1479
1480> Example App: check out https://github.com/zapier/zapier-platform-example-app-middleware for a working example app using HTTP middleware.
1481
1482Try putting them in your app definition:
1483
1484```js
1485const addHeader = (request, z, bundle) => {
1486 request.headers['my-header'] = 'from zapier';
1487 return request;
1488};
1489
1490const mustBe200 = (response, z, bundle) => {
1491 if (response.status !== 200) {
1492 throw new Error(`Unexpected status code ${response.status}`);
1493 }
1494 return response;
1495};
1496
1497const autoParseJson = (response, z, bundle) => {
1498 response.json = z.JSON.parse(response.content);
1499 return response;
1500};
1501
1502const App = {
1503 // ...
1504 beforeRequest: [addHeader],
1505 afterResponse: [mustBe200, autoParseJson]
1506 // ...
1507};
1508
1509```
1510
1511A `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.
1512
1513Middleware functions can be asynchronous - just return a promise from the middleware function.
1514
1515The second argument for middleware is the `z` object, but it does *not* include `z.request()` as using that would easily create infinite loops.
1516
1517### HTTP Request Options
1518
1519Shorthand requests and manual `z.request([url], options)` calls support the following HTTP `options`:
1520
1521* `url`: HTTP url, you can provide it both `z.request(url, options)` or `z.request({url: url, ...})`.
1522* `method`: HTTP method, default is `GET`.
1523* `headers`: request headers object, format `{'header-key': 'header-value'}`.
1524* `params`: URL query params object, format `{'query-key': 'query-value'}`.
1525* `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`.
1526* `json`: shortcut object/array/etc. you want to JSON encode into body. Default is `null`.
1527* `form`: shortcut object. you want to form encode into body. Default is `null`.
1528* `raw`: set this to stream the response instead of consuming it immediately. Default is `false`.
1529* `redirect`: set to `manual` to extract redirect headers, `error` to reject redirect, default is `follow`.
1530* `follow`: maximum redirect count, set to `0` to not follow redirects. default is `20`.
1531* `compress`: support gzip/deflate content encoding. Set to `false` to disable. Default is `true`.
1532* `agent`: Node.js `http.Agent` instance, allows custom proxy, certificate etc. Default is `null`.
1533* `timeout`: request / response timeout in ms. Set to `0` to disable (OS limit still applies), timeout reset on `redirect`. Default is `0` (disabled).
1534* `size`: maximum response body size in bytes. Set to `0` to disable. Default is `0` (disabled).
1535
1536```js
1537z.request({
1538 url: 'http://example.com',
1539 method: 'POST',
1540 headers: {
1541 'Content-Type': 'application/json'
1542 },
1543 // only provide body, json or form...
1544 body: {hello: 'world'}, // or '{"hello": "world"}' or 'hello=world'
1545 json: {hello: 'world'},
1546 form: {hello: 'world'},
1547 // access node-fetch style response.body
1548 raw: false,
1549 redirect: 'follow',
1550 follow: 20,
1551 compress: true,
1552 agent: null,
1553 timeout: 0,
1554 size: 0,
1555})
1556```
1557
1558### HTTP Response Object
1559
1560The response object returned by `z.request([url], options)` supports the following fields and methods:
1561
1562* `status`: The response status code, i.e. `200`, `404`, etc.
1563* `content`: The response content as a String. For Buffer, try `options.raw = true`.
1564* `json`: The response content as an object (or `undefined`). If `options.raw = true` - is a promise.
1565* `body`: A stream available only if you provide `options.raw = true`.
1566* `headers`: Response headers object. The header keys are all lower case.
1567* `getHeader(key)`: Retrieve response header, case insensitive: `response.getHeader('My-Header')`
1568* `throwForStatus()`: Throw error if final `response.status > 300`. Will throw `z.error.RefreshAuthError` if 401.
1569* `request`: The original request options object (see above).
1570
1571```js
1572z.request({
1573 // ..
1574}).then((response) => {
1575 // a bunch of examples lines for cherry picking
1576 response.status;
1577 response.headers['Content-Type'];
1578 response.getHeader('content-type');
1579 response.request; // original request options
1580 response.throwForStatus();
1581 // if options.raw === false (default)...
1582 JSON.parse(response.content);
1583 response.json;
1584 // if options.raw === true...
1585 response.buffer().then(buf => buf.toString());
1586 response.text().then(content => content);
1587 response.json().then(json => json);
1588 response.body.pipe(otherStream);
1589});
1590```
1591
1592
1593## Dehydration
1594
1595Dehydration, and its counterpart Hydration, is a tool that can lazily load data that might be otherwise expensive to retrieve aggressively.
1596
1597* **Dehydration** - think of this as "make a pointer", you control the creation of pointers with `z.dehydrate(func, inputData)`
1598* **Hydration** - think of this as an automatic step that "consumes a pointer" and "returns some data", Zapier does this automatically behind the scenes
1599
1600> This is very common when [Stashing Files](#stashing-files) - but that isn't their only use!
1601
1602The method `z.dehydrate(func, inputData)` has two required arguments:
1603
1604* `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
1605* `inputData` - this is an object that contains things like a `path` or `id` - whatever you need to load data on the other side
1606
1607> **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.
1608
1609Here is an example that pulls in extra data for a movie:
1610
1611```js
1612const getExtraDataFunction = (z, bundle) => {
1613 const url = `http://example.com/movies/${bundle.inputData.id}.json`;
1614 return z.request(url).then(res => z.JSON.parse(res.content));
1615};
1616
1617const movieList = (z, bundle) => {
1618 return z
1619 .request('http://example.com/movies.json')
1620 .then(res => z.JSON.parse(res.content))
1621 .then(results => {
1622 return results.map(result => {
1623 // so maybe /movies.json is thin content but
1624 // /movies/:id.json has more details we want...
1625 result.moreData = z.dehydrate(getExtraDataFunction, {
1626 id: result.id
1627 });
1628 return result;
1629 });
1630 });
1631};
1632
1633const App = {
1634 version: require('./package.json').version,
1635 platformVersion: require('zapier-platform-core').version,
1636
1637 // don't forget to register hydrators here!
1638 // it can be imported from any module
1639 hydrators: {
1640 getExtraData: getExtraDataFunction
1641 },
1642
1643 triggers: {
1644 new_movie: {
1645 noun: 'Movie',
1646 display: {
1647 label: 'New Movie',
1648 description: 'Triggers when a new Movie is added.'
1649 },
1650 operation: {
1651 perform: movieList
1652 }
1653 }
1654 }
1655};
1656
1657module.exports = App;
1658
1659```
1660
1661And 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.
1662
1663> **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.
1664
1665
1666## Stashing Files
1667
1668It 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.
1669
1670The 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:
1671
1672```js
1673const content = 'Hello world!';
1674z.stashFile(content, content.length, 'hello.txt', 'text/plain')
1675 .then(url => z.console.log(url));
1676// https://zapier-dev-files.s3.amazonaws.com/cli-platform/f75e2819-05e2-41d0-b70e-9f8272f9eebf
1677```
1678
1679Most likely you'd want to stream from another URL - note the usage of `z.request({raw: true})`:
1680
1681```js
1682const fileRequest = z.request({url: 'http://example.com/file.pdf', raw: true});
1683z.stashFile(fileRequest) // knownLength and filename will be sniffed from the request. contentType will be binary/octet-stream
1684 .then(url => z.console.log(url));
1685// https://zapier-dev-files.s3.amazonaws.com/cli-platform/74bc623c-d94d-4cac-81f1-f71d7d517bc7
1686```
1687
1688> Note: you should only be using `z.stashFile()` in a hydration method - otherwise it can be very expensive to stash dozens of files in a polling call - for example!
1689
1690See a full example with dehydration/hydration wired in correctly:
1691
1692```js
1693const stashPDFfunction = (z, bundle) => {
1694 // use standard auth to request the file
1695 const filePromise = z.request({
1696 url: bundle.inputData.downloadUrl,
1697 raw: true
1698 });
1699 // and swap it for a stashed URL
1700 return z.stashFile(filePromise);
1701};
1702
1703const pdfList = (z, bundle) => {
1704 return z
1705 .request('http://example.com/pdfs.json')
1706 .then(res => z.JSON.parse(res.content))
1707 .then(results => {
1708 return results.map(result => {
1709 // lazily convert a secret_download_url to a stashed url
1710 // zapier won't do this until we need it
1711 result.file = z.dehydrate(stashPDFfunction, {
1712 downloadUrl: result.secret_download_url
1713 });
1714 delete result.secret_download_url;
1715 return result;
1716 });
1717 });
1718};
1719
1720const App = {
1721 version: require('./package.json').version,
1722 platformVersion: require('zapier-platform-core').version,
1723
1724 hydrators: {
1725 stashPDF: stashPDFfunction
1726 },
1727
1728 triggers: {
1729 new_pdf: {
1730 noun: 'PDF',
1731 display: {
1732 label: 'New PDF',
1733 description: 'Triggers when a new PDF is added.'
1734 },
1735 operation: {
1736 perform: pdfList
1737 }
1738 }
1739 }
1740};
1741
1742module.exports = App;
1743
1744```
1745
1746> Example App: check out https://github.com/zapier/zapier-platform-example-app-files for a working example app using files.
1747
1748
1749## Logging
1750
1751There 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).
1752
1753To 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).
1754
1755For advanced logging options including only displaying the logs for a certain user or app version, look at the help for the logs command:
1756
1757```bash
1758zapier help logs
1759```
1760
1761### Console Logging
1762
1763To manually print a log statement in your code, use `z.console.log`:
1764
1765```js
1766z.console.log('Here are the input fields', bundle.inputData);
1767```
1768
1769The `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).
1770
1771### Viewing Console Logs
1772
1773To see your `z.console.log` logs, do:
1774
1775```bash
1776zapier logs --type=console
1777```
1778
1779### Viewing Bundle Logs
1780
1781To see the bundle logs, do:
1782
1783```bash
1784zapier logs --type=bundle
1785```
1786
1787### HTTP Logging
1788
1789If you are using the `z.request()` shortcut that we provide - HTTP logging is handled automatically for you. For example:
1790
1791```js
1792z.request('http://57b20fb546b57d1100a3c405.mockapi.io/api/recipes')
1793 .then((res) => {
1794 // do whatever you like, this request is already getting logged! :-D
1795 return res;
1796 })
1797```
1798
1799### Viewing HTTP Logs
1800
1801To see the HTTP logs, do:
1802
1803```bash
1804zapier logs --type=http
1805```
1806To see detailed http logs including headers, request and response bodies, etc, do:
1807
1808```bash
1809zapier logs --type=http --detailed
1810```
1811
1812
1813## Error Handling
1814
1815APIs are not always available. Users do not always input data correctly to
1816formulate valid requests. Thus, it is a good idea to write apps defensively and
1817plan for 4xx and 5xx responses from APIs. Without proper handling, errors often
1818have incomprehensible messages for end users, or possibly go uncaught.
1819
1820Zapier provides a couple tools to help with error handling. First is the `afterResponse`
1821middleware ([docs](#using-http-middleware)), which provides a hook for processing
1822all responses from HTTP calls. The other tool is the collection of errors in
1823`z.errors` ([docs](#zerrors)), which control the behavior of Zaps when
1824various kinds of errors occur.
1825
1826### General Errors
1827
1828Errors due to a misconfiguration in a user's Zap should be handled in your app
1829by throwing a standard JavaScript `Error` with a user-friendly message.
1830Typically, this will be prettifying 4xx responses or APIs that return errors as
1831200s with a payload that describes the error.
1832
1833Example: `throw new Error('Your error message.');`
1834
1835A couple best practices to keep in mind:
1836
1837 * Elaborate on terse messages. "not_authenticated" -> "Your API Key is invalid. Please reconnect your account."
1838 * If the error calls out a specific field, surface that information to the user. "Invalid Request" -> "contact name is invalid"
1839 * If the error provides details about why a field is invalid, add that in too! "contact name is invalid" -> "contact name is too long"
1840
1841Note that if a Zap raises too many error messages it will be automatically
1842turned off, so only use these if the scenario is truly an error that needs to
1843be fixed.
1844
1845### Halting Execution
1846
1847Any operation can be interrupted or "halted" (not success, not error, but
1848stopped for some specific reason) with a `HaltedError`. You might find yourself
1849using this error in cases where a required pre-condition is not met. For instance,
1850in a create to add an email address to a list where duplicates are not allowed,
1851you would want to throw a `HaltedError` if the Zap attempted to add a duplicate.
1852This would indicate failure, but it would be treated as a soft failure.
1853
1854Unlike throwing `Error`, a Zap will never by turned off when this error is thrown
1855(even if it is raised more often than not).
1856
1857Example: `throw new z.errors.HaltedError('Your reason.');`
1858
1859### Stale Authentication Credentials
1860
1861For apps that require manual refresh of authorization on a regular basis, Zapier
1862provides a mechanism to notify users of expired credentials. With the
1863`ExpiredAuthError`, the current operation is interrupted, the Zap is turned off
1864(to prevent more calls with expired credentials), and a predefined email is sent
1865out informing the user to refresh the credentials.
1866
1867Example: `throw new z.errors.ExpiredAuthError('Your message.');`
1868
1869For apps that use OAuth2 + refresh or Session Auth, you can use the
1870`RefreshAuthError`. This will signal Zapier to refresh the credentials and then
1871repeat the failed operation.
1872
1873Example: `throw new z.errors.RefreshAuthError();`
1874
1875
1876## Testing
1877
1878You can write unit tests for your Zapier app that run locally, outside of the zapier editor.
1879You can run these tests in a CI tool like [Travis](https://travis-ci.com/).
1880
1881### Writing Unit Tests
1882
1883We recommend using the [Mocha](https://mochajs.org/) testing framework. After running
1884`zapier init` you should find an example test to start from in the `test` directory.
1885
1886```js
1887// we use should assertions
1888const should = require('should');
1889const zapier = require('zapier-platform-core');
1890
1891// createAppTester() makes it easier to test your app. It takes your
1892// raw app definition, and returns a function that will test you app.
1893const App = require('../index');
1894const appTester = zapier.createAppTester(App);
1895
1896describe('triggers', () => {
1897 describe('new recipe trigger', () => {
1898 it('should load recipes', done => {
1899 // This is what Zapier will send to your app as input.
1900 // It contains trigger options the user choice in their zap.
1901 const bundle = {
1902 inputData: {
1903 style: 'mediterranean'
1904 }
1905 };
1906
1907 // Pass appTester the path to the trigger you want to call,
1908 // and the input bundle. appTester returns a promise for results.
1909 appTester(App.App.triggers.recipe.operation.perform, bundle)
1910 .then(results => {
1911 // Make assertions
1912
1913 results.length.should.eql(10);
1914
1915 const firstRecipe = results[0];
1916 firstRecipe.name.should.eql('Baked Falafel');
1917
1918 done();
1919 })
1920 .catch(done);
1921 });
1922 });
1923});
1924
1925```
1926
1927### Mocking Requests
1928
1929While 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`.
1930
1931```js
1932require('should');
1933
1934const zapier = require('zapier-platform-core');
1935
1936const App = require('../index');
1937const appTester = zapier.createAppTester(App);
1938
1939const nock = require('nock');
1940
1941describe('triggers', () => {
1942 describe('new recipe trigger', () => {
1943 it('should load recipes', done => {
1944 const bundle = {
1945 inputData: {
1946 style: 'mediterranean'
1947 }
1948 };
1949
1950 // mocks the next request that matches this url and querystring
1951 nock('http://57b20fb546b57d1100a3c405.mockapi.io/api')
1952 .get('/recipes')
1953 .query(bundle.inputData)
1954 .reply(200, [
1955 { name: 'name 1', directions: 'directions 1', id: 1 },
1956 { name: 'name 2', directions: 'directions 2', id: 2 }
1957 ]);
1958
1959 appTester(App.triggers.recipe.operation.perform, bundle)
1960 .then(results => {
1961 results.length.should.above(1);
1962
1963 const firstRecipe = results[0];
1964 firstRecipe.name.should.eql('name 1');
1965 firstRecipe.directions.should.eql('directions 1');
1966
1967 done();
1968 })
1969 .catch(done);
1970 });
1971
1972 it('should load recipes without filters', done => {
1973 const bundle = {};
1974
1975 // each test needs its own mock
1976 nock('http://57b20fb546b57d1100a3c405.mockapi.io/api')
1977 .get('/recipes')
1978 .reply(200, [
1979 { name: 'name 1', directions: 'directions 1', id: 1 },
1980 { name: 'name 2', directions: 'directions 2', id: 2 }
1981 ]);
1982
1983 appTester(App.triggers.recipe.operation.perform, bundle)
1984 .then(results => {
1985 results.length.should.above(1);
1986
1987 const firstRecipe = results[0];
1988 firstRecipe.name.should.eql('name 1');
1989 firstRecipe.directions.should.eql('directions 1');
1990
1991 done();
1992 })
1993 .catch(done);
1994 });
1995 });
1996});
1997
1998```
1999
2000There's more info about nock and its usage in its [readme](https://github.com/node-nock/nock/blob/master/README.md).
2001
2002### Running Unit Tests
2003
2004To run all your tests do:
2005
2006```bash
2007zapier test
2008```
2009
2010> You can also go direct with `npm test` or `node_modules/mocha/bin/mocha`.
2011
2012### Testing & Environment Variables
2013
2014The 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:
2015
2016```js
2017const zapier = require('zapier-platform-core');
2018zapier.tools.env.inject(); // inject() can take a filename; defaults to ".env"
2019
2020// now process.env has all the values in your .env file
2021```
2022
2023> `.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.
2024
2025> Remember: **NEVER** add your secrets file to version control!
2026
2027Additionally, you can provide them dynamically at runtime:
2028
2029```bash
2030CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2031```
2032
2033Or, `export` them explicitly and place them into the environment:
2034
2035```bash
2036export CLIENT_ID=1234
2037export CLIENT_SECRET=abcd
2038zapier test
2039```
2040
2041
2042### Viewing HTTP Logs in Unit Tests
2043
2044
2045When running a unit test via `zapier test`, `z.console` statements and detailed HTTP logs print to `stdout`:
2046
2047```bash
2048zapier test
2049```
2050
2051Sometimes you don't want that much logging, for example in a CI test. To suppress the detailed HTTP logs do:
2052
2053```bash
2054zapier test --quiet
2055```
2056
2057To also suppress the HTTP summary logs do:
2058
2059```bash
2060zapier test --very-quiet
2061```
2062
2063### Testing in Your CI
2064
2065Whether you use Travis, Circle, Jenkins, or anything else, we aim to make it painless to test in an automated environment.
2066
2067Behind the scenes `zapier test` is doing a pretty standard `npm test` with [mocha](https://www.npmjs.com/package/mocha) as the backend.
2068
2069This 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:
2070
2071```yaml
2072language: node_js
2073node_js:
2074 - "v8.10.0"
2075before_script: npm install -g zapier-platform-cli
2076script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
2077```
2078
2079You 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!
2080
2081As 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).
2082
2083
2084## Using `npm` Modules
2085
2086Use `npm` modules just like you would use them in any other node app, for example:
2087
2088```bash
2089npm install --save jwt
2090```
2091
2092And then `package.json` will be updated, and you can use them like anything else:
2093
2094```js
2095const jwt = require('jwt');
2096```
2097
2098During the `zapier build` or `zapier push` step - we'll copy all your code to `/tmp` folder and do a fresh re-install of modules.
2099
2100> 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`.
2101
2102> 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:
2103
2104```json
2105{
2106 "id": 1,
2107 "key": "App1",
2108 "includeInBuild": [
2109 "test.txt",
2110 "testing.json"
2111 ]
2112}
2113
2114```
2115
2116> Warning: do not use compiled libraries unless you run your build on the AWS AMI `ami-6869aa05`.
2117
2118
2119## Using Transpilers
2120
2121We 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`:
2122
2123```json
2124{
2125 "scripts": {
2126 "zapier-dev": "babel src --out-dir lib --watch",
2127 "zapier-push": "babel src --out-dir lib && zapier push"
2128 }
2129}
2130```
2131
2132And then you can have your fancy ES7 code in `src/*` and a root `index.js` like this:
2133
2134```js
2135module.exports = require('./lib');
2136```
2137
2138And work with commands like this:
2139
2140```bash
2141# watch and recompile
2142npm run zapier-dev
2143
2144# tests should work fine
2145zapier test
2146
2147# every build ensures a fresh build
2148npm run zapier-push
2149```
2150
2151There are a lot of details left out - check out the full example app for a working setup.
2152
2153> Example App: check out https://github.com/zapier/zapier-platform-example-app-babel for a working example app using Babel.
2154
2155## Example Apps
2156
2157See [the wiki](https://github.com/zapier/zapier-platform-cli/wiki/Example-Apps) for a full list of working examples (and installation instructions).
2158
2159## FAQs
2160
2161### Why doesn't Zapier support newer versions of Node.js?
2162
2163We run your code on AWS Lambda, which only supports a few [versions](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) of Node (the latest of which is `v8.10.0`. As that updates, so too will we.
2164
2165### How do I manually set the Node.js version to run my app with?
2166
2167Update 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/current-supported-versions.html).
2168
2169### When to use placeholders or curlies?
2170
2171You 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.
2172
2173In general, use `${var}` within functions and use `{{var}}` anywhere else.
2174
2175Placeholders 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.
2176
2177> 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>.
2178
2179### Does Zapier support XML (SOAP) APIs?
2180
2181Not natively, but it can! Users have reported that the following `npm` modules are compatible with the CLI Platform:
2182
2183* [pixl-xml](https://github.com/jhuckaby/pixl-xml)
2184* [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js)
2185* [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser)
2186
2187```js
2188const xml = require('pixl-xml');
2189
2190const App = {
2191 // ...
2192 afterResponse: [
2193 (response, z, bundle) => {
2194 response.xml = xml.parse(response.content);
2195 return response;
2196 }
2197 ]
2198 // ...
2199};
2200
2201```
2202
2203### Is it possible to iterate over pages in a polling trigger?
2204
2205Yes, 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).
2206
2207```js
2208// some async call
2209const makeCall = (z, start, limit) => {
2210 return z.request({
2211 url: 'https://jsonplaceholder.typicode.com/posts',
2212 params: {
2213 _start: start,
2214 _limit: limit
2215 }
2216 });
2217};
2218
2219// triggers on paging with a certain tag
2220const performPaging = (z, bundle) => {
2221 const limit = 3;
2222 let start = 0;
2223
2224 // array of promises
2225 let promises = [];
2226
2227 let i = 0;
2228 while (i < 5) {
2229 promises.push(makeCall(z, start, limit));
2230 start += limit;
2231 i += 1;
2232 }
2233
2234 return Promise.all(promises).then(res => {
2235 // res is an array of responses
2236 const results = res.map(r => r.json); // array of arrays of js objects
2237 return Array.prototype.concat.apply([], results); // flatten array
2238 });
2239};
2240
2241module.exports = {
2242 key: 'paging',
2243 noun: 'Paging',
2244
2245 display: {
2246 label: 'Get Paging',
2247 description: 'Triggers on a new paging.'
2248 },
2249
2250 operation: {
2251 inputFields: [],
2252 perform: performPaging
2253 }
2254};
2255
2256```
2257
2258If you need to do more requests conditionally based on the results of an HTTP call (such as getting "next url" param, using `async/await` with a transpiler is the 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.
2259
2260```js
2261// a hypothetical API where payloads are big so we want to heavily limit how much comes back
2262// we want to only return items created in the last hour
2263
2264const asyncExample = async (z, bundle) => {
2265 const limit = 3;
2266 let start = 0;
2267 const twoHourMilliseconds = 60 * 60 * 2 * 1000;
2268 const hoursAgo = new Date() - twoHourMilliseconds;
2269
2270 let response = await z.request({
2271 url: 'https://jsonplaceholder.typicode.com/posts',
2272 params: {
2273 _start: start,
2274 _limit: limit
2275 }
2276 });
2277
2278 let results = response.json;
2279
2280 // keep paging until the last item was created over two hours ago
2281 // then we know we almost certainly haven't missed anything and can let
2282 // deduper handle the rest
2283
2284 while (new Date(results[results.length - 1].createdAt) > hoursAgo) {
2285 start += limit; // next page
2286
2287 response = await z.request({
2288 url: 'https://jsonplaceholder.typicode.com/posts',
2289 params: {
2290 _start: start,
2291 _limit: limit
2292 }
2293 });
2294
2295 results = results.concat(response.json);
2296 }
2297
2298 return results;
2299};
2300
2301```
2302
2303### How do search-powered fields relate to dynamic dropdowns and why are they both required together?
2304
2305To understand search-powered fields, we have to have a good understanding of dynamic dropdowns.
2306
2307When 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:
2308
2309![](https://cdn.zapier.com/storage/photos/fb56bdc2aab91504be0e51800bec4d64.png)
2310
2311The 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.
2312
2313**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:
2314
23151. Some Trigger
23162. Calculate what day of the week it is today (Code)
23173. Find the spreadsheet that matches the day from Step 2
23184. Update the spreadsheet (with the id from step 3) with some data
2319
2320If 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.
2321
2322![](https://cdn.zapier.com/storage/photos/d263fd3a56cf8108cb89195163e7c9aa.png)
2323
2324This is paired most often with "update" actions, where a required parameter will be a resource id.
2325
2326<a id="paging"></a>
2327### What's the deal with pagination? When is it used and how does it work?
2328
2329Paging 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.
2330
2331Paging 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:
2332
2333```js
2334(z, bundle) => {
2335 const promise = z.request({
2336 url: 'http://example.com/api/list.json',
2337 params: {
2338 limit: 100,
2339 offset: 100 * bundle.meta.page
2340 }
2341 });
2342 return promise.then((response) => response.json);
2343};
2344```
2345
2346If your API uses cursor-based paging instead of an offset, you can use `z.cursor.get` and `z.cursor.set`:
2347
2348```js
2349// the perform method of our trigger
2350// ensure operation.canPaginate is true!
2351
2352const performWithoutAsync = (z, bundle) => {
2353 return Promise.resolve()
2354 .then(() => {
2355 if (bundle.meta.page === 0) {
2356 // first page, no need to fetch a cursor
2357 return Promise.resolve();
2358 } else {
2359 return z.cursor.get(); // Promise<string | null>
2360 }
2361 })
2362 .then(cursor => {
2363 return z.request(
2364 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
2365 {
2366 params: { cursor: cursor } // if cursor is null, it's ignored here
2367 }
2368 );
2369 })
2370 .then(response => {
2371 // need to save the cursor and return a promise, but also need to pass the data along
2372 return Promise.all([response.items, z.cursor.set(response.nextPage)]);
2373 })
2374 .then(([items /* null */]) => {
2375 return items;
2376 });
2377};
2378
2379// ---------------------------------------------------
2380
2381const performWithAsync = async (z, bundle) => {
2382 let cursor;
2383 if (bundle.meta.page) {
2384 cursor = await z.cursor.get(); // string | null
2385 }
2386
2387 const response = await z.request(
2388 'https://5ae7ad3547436a00143e104d.mockapi.io/api/recipes',
2389 {
2390 // if cursor is null, it's sent as an empty query
2391 // param and should be ignored by the server
2392 params: { cursor: cursor }
2393 }
2394 );
2395
2396 // we successfully got page 1, should store the cursor in case the user wants page 2
2397 await z.cursor.set(response.nextPage);
2398
2399 return response.items;
2400};
2401
2402```
2403
2404Cursors 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.
2405
2406Lastly, 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.
2407
2408<a id="dedup"></a>
2409### How does deduplication work?
2410
2411Each 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.
2412
2413For 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.
2414
2415There's a more in-depth explanation [here](https://zapier.com/developer/documentation/v2/deduplication/).
2416
2417### Why are my triggers complaining if I don't provide an explicit `id` field? I didn't have to do that in the Web Builder!
2418
2419For deduplication to work, we need to be able to identify and use a unique field. For WB 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:
2420
2421```js
2422// ...
2423let items = z.JSON.parse(response.content).items;
2424items.forEach(item => {
2425 item.id = item.contactId;
2426})
2427
2428return items;
2429```
2430
2431## Command Line Tab Completion
2432
2433We have provided two tab completion scripts to make it easier to use the Zapier Platform CLI, for zsh and bash.
2434
2435### Zsh Completion Script
2436
2437To 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:
2438
2439```zsh
2440# add custom completion scripts
2441fpath=(~/.zsh/completion $fpath)
2442
2443# compsys initialization
2444autoload -U compinit
2445compinit
2446```
2447
2448Next download our completion script to your completions directory:
2449
2450```zsh
2451cd ~/.zsh/completion
2452curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/zsh/_zapier -O
2453```
2454
2455Finally, restart your shell and start hitting TAB with the `zapier` command!
2456
2457### Bash Completion Script
2458
2459To 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.
2460
2461```bash
2462cd ~/.bash_completion.d
2463curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/bash/_zapier -O
2464```
2465
2466Next source the script from your `~/.bash_profile`:
2467
2468```bash
2469source ~/.bash_completion.d/_zapier
2470```
2471
2472Finally, restart your shell and start hitting TAB with the `zapier` command!
2473
2474## The Zapier Platform Packages
2475
2476The Zapier Platform consists of 3 npm packages that are released simultaneously as a trio.
2477
2478- [`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.
2479
2480- [`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.
2481
2482- [`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.
2483
2484### Updating
2485
2486The 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`).
2487
2488Barring 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.
2489
2490<!-- TODO: if we decouple releases, change this -->
2491The most recently released version of `cli` and `core` is `7.1.0`. You can see the versions you're working with by running `zapier -v`.
2492
2493To update `cli`, run `npm install -g zapier-platform-cli`.
2494
2495To 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`.
2496
2497For maximum compatibility, keep the versions of `cli` and `core` in sync.
2498
2499## Development of the CLI
2500
2501This section is only relevant if you're editing the `zapier-platform-cli` package itself.
2502
2503### Commands
2504
2505- `export ZAPIER_BASE_ENDPOINT='http://localhost:8001'` if you're building against a local dev environment
2506- `npm install` for getting started
2507- `npm run build` for updating `./lib` from `./src`
2508- `npm test` for running tests (also runs `npm run build`)
2509- `npm run test-convert` for running integration tests for the `zapier convert` command
2510- `npm run docs` for updating docs
2511- `npm run gen-completions` for updating the auto complete scripts
2512
2513### Publishing of the CLI (after merging)
2514
2515- `npm version [patch|minor|major]` will pull, test, update docs, increment version in package.json, push tags, and publish to npm
2516- `npm run validate-templates` for validating the example apps
2517- `npm run set-template-versions VERSION` for updating the platform-core version in the example app repos to `VERSION`
2518
2519## Get Help!
2520
2521You can get help by either emailing partners@zapier.com or by joining our Slack channel https://zapier-platform-slack.herokuapp.com.