1 |
|
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 |
|
15 | </p>
|
16 |
|
17 | Zapier 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 |
|
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 |
|
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 |
|
131 | A CLI App is an implementation of your app's API. You build a Node.js application
|
132 | that exports a single object ([JSON Schema](https://zapier.github.io/zapier-platform-schema/build/schema.html#appschema)) and upload it to Zapier.
|
133 | Zapier introspects that definition to find out what your app is capable of and
|
134 | what options to present end users in the Zap Editor.
|
135 |
|
136 | For those not familiar with Zapier terminology, here is how concepts in the CLI
|
137 | map 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 |
|
148 | Zapier takes the App you upload and sends it over to Amazon Web Service's Lambda.
|
149 | We then make calls to execute the operations your App defines as we execute Zaps.
|
150 | Your App takes the input data we provide (if any), makes the necessary HTTP calls,
|
151 | and returns the relevant data, which gets fed back into Zapier.
|
152 |
|
153 | ### CLI vs the Web Builder Platform
|
154 |
|
155 | From 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 |
|
159 | All Zapier CLI apps are run using Node.js `v8.10.0`.
|
160 |
|
161 | You 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 |
|
163 | To 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 |
|
165 | Firstly, 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 |
|
167 | Alternatively, 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).
|
168 | Then 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 |
|
173 | First 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
|
177 | npm install -g zapier-platform-cli
|
178 |
|
179 | # setup auth to Zapier's platform with a deploy key
|
180 | zapier login
|
181 | ```
|
182 |
|
183 | Your 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
|
187 | zapier init example-app
|
188 |
|
189 | # move into the new directory
|
190 | cd example-app
|
191 |
|
192 | # install all the libraries needed for your app
|
193 | npm install
|
194 | ```
|
195 |
|
196 | > Note: there are plenty of templates & example apps to choose from! [View all Example Apps here.](#example-apps)
|
197 |
|
198 | You 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
|
203 | zapier test
|
204 | ```
|
205 |
|
206 | Next, 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
|
210 | zapier 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 |
|
218 | For 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 |
|
224 | Creating 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
|
228 | mkdir zapier-example
|
229 | cd zapier-example
|
230 |
|
231 | # create the needed files from a template
|
232 | zapier init . --template=trigger
|
233 |
|
234 | # install all the libraries needed for your app
|
235 | npm install
|
236 | ```
|
237 |
|
238 | If 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 |
|
249 | In 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 |
|
274 | The 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
|
277 | const 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 |
|
302 | module.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 |
|
311 | Registering 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
|
315 | zapier register "Zapier Example"
|
316 |
|
317 | # list your apps
|
318 | zapier 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 |
|
323 | If 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 |
|
335 | An 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
|
339 | zapier push
|
340 |
|
341 | # list your versions
|
342 | zapier versions
|
343 | ```
|
344 |
|
345 | If 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 |
|
359 | A 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 |
|
364 | This 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
|
368 | zapier 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)
|
371 | zapier collaborate user@example.com
|
372 | ```
|
373 |
|
374 | You 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 |
|
379 | Promotion 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 |
|
381 | If 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
|
385 | zapier promote 1.0.1
|
386 |
|
387 | # OPTIONAL - migrate your users between one app version to another
|
388 | zapier migrate 1.0.0 1.0.1
|
389 |
|
390 | # OR - mark the old version as deprecated
|
391 | zapier deprecate 1.0.0 2017-01-01
|
392 | ```
|
393 |
|
394 | ## Converting an Existing App
|
395 |
|
396 | If 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
|
401 | zapier convert 1234 my-app
|
402 | ```
|
403 |
|
404 | Your 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 |
|
412 | Most 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 |
|
418 | Useful 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
|
425 | const 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 |
|
435 | const App = {
|
436 | // ...
|
437 | authentication: authentication
|
438 | // ...
|
439 | };
|
440 |
|
441 | ```
|
442 |
|
443 | ### Custom
|
444 |
|
445 | This 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
|
450 | const 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 |
|
473 | const 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 |
|
480 | const App = {
|
481 | // ...
|
482 | authentication: authentication,
|
483 | beforeRequest: [addApiKeyToHeader]
|
484 | // ...
|
485 | };
|
486 |
|
487 | ```
|
488 |
|
489 | ### Session
|
490 |
|
491 | Probably 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
|
496 | const 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 |
|
516 | const 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 |
|
546 | const 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 |
|
554 | const 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 |
|
563 | const 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 |
|
577 | Zapier'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 |
|
581 | It 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 |
|
589 | You 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 |
|
600 | Your auth definition would look something like this:
|
601 |
|
602 | ```js
|
603 | const 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 |
|
653 | const 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 |
|
660 | const App = {
|
661 | // ...
|
662 | authentication: authentication,
|
663 | beforeRequest: [addBearerHeader]
|
664 | // ...
|
665 | };
|
666 |
|
667 | module.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 |
|
676 | A `resource` is a representation (as a JavaScript object) of one of the REST resources of your API. Say you have a `/recipes`
|
677 | endpoint for working with recipes; you can define a recipe resource in your app that will tell Zapier how to do create,
|
678 | read, and search operations on that resource.
|
679 |
|
680 | ```js
|
681 | const 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 |
|
697 | The quickest way to create a resource is with the `zapier scaffold` command:
|
698 |
|
699 | ```bash
|
700 | zapier scaffold resource "Recipe"
|
701 | ```
|
702 |
|
703 | This will generate the resource file and add the necessary statements to the `index.js` file to import it.
|
704 |
|
705 |
|
706 | ### Resource Definition
|
707 |
|
708 | A resource has a few basic properties. The first is the `key`, which allows Zapier to identify the resource on our backend.
|
709 | The 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 |
|
713 | After those, there is a set of optional properties that tell Zapier what methods can be performed on the resource.
|
714 | The complete list of available methods can be found in the [Resource Schema Docs](https://zapier.github.io/zapier-platform-schema/build/schema.html#resourceschema).
|
715 | For 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 |
|
720 | Here is a complete example of what the list method might look like
|
721 |
|
722 | ```js
|
723 | const listRecipesRequest = {
|
724 | url: 'http://example.com/recipes'
|
725 | };
|
726 |
|
727 | const 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 |
|
743 | The 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 |
|
745 | Adding a create method looks very similar.
|
746 |
|
747 | ```js
|
748 | const createRecipeRequest = {
|
749 | url: 'http://example.com/recipes',
|
750 | method: 'POST',
|
751 | body: {
|
752 | name: 'Baked Falafel',
|
753 | style: 'mediterranean'
|
754 | }
|
755 | };
|
756 |
|
757 | const 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 |
|
776 | Every method you define on a `resource` Zapier converts to the appropriate Trigger, Create, or Search. Our examples
|
777 | above would result in an app with a New Recipe Trigger and an Add Recipe Create.
|
778 |
|
779 | Note 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 |
|
784 | Triggers, Searches, and Creates are the way an app defines what it is able to do. Triggers read
|
785 | data into Zapier (i.e. watch for new recipes). Searches locate individual records (find recipe by title). Creates create
|
786 | new records in your system (add a recipe to the catalog).
|
787 |
|
788 | The definition for each of these follows the same structure. Here is an example of a trigger:
|
789 |
|
790 | ```js
|
791 | const recipeListRequest = {
|
792 | url: 'http://example.com/recipes'
|
793 | };
|
794 |
|
795 | const 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 |
|
819 | You 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 |
|
832 | Each 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 |
|
842 | On 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 |
|
844 | You 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 |
|
846 | Those fields have various options you can provide, here is a succinct example:
|
847 |
|
848 | ```js
|
849 | const 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 |
|
879 | In 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
|
886 | const 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 |
|
892 | const 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 |
|
921 | Additionally, 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
|
924 | module.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 |
|
958 | Sometimes, 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 |
|
960 | Our 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 |
|
962 | To 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
|
965 | const 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 |
|
1008 | In 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 |
|
1016 | For 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
|
1019 | const 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 |
|
1065 | If you don't define a trigger for the `dynamic` property, the search connector won't show.
|
1066 |
|
1067 | ### Computed Fields
|
1068 |
|
1069 | In 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 |
|
1071 | For 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 |
|
1075 | We 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 |
|
1107 | The 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 |
|
1114 | For more details on error handling in general, see [here](#error-handling).
|
1115 |
|
1116 | ### `z.cursor`
|
1117 |
|
1118 | The `z.cursor` object exposes two methods:
|
1119 |
|
1120 | * `z.cursor.get(): Promise<string|null>`
|
1121 | * `z.cursor.set(string): Promise<null>`
|
1122 |
|
1123 | Any 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 |
|
1127 | This 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 |
|
1176 | The user's Zap ID is available during the [subscribe and unsubscribe](https://zapier.github.io/zapier-platform-schema/build/schema.html#basichookoperationschema) methods.
|
1177 |
|
1178 | For example - you could do:
|
1179 |
|
1180 | ```js
|
1181 | const 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 |
|
1195 | module.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 |
|
1244 | Apps can define environment variables that are available when the app's code executes. They work just like environment
|
1245 | variables defined on the command line. They are useful when you have data like an OAuth client ID and secret that you
|
1246 | don't want to commit to source control. Environment variables can also be used as a quick way to toggle between a
|
1247 | a staging and production environment during app development.
|
1248 |
|
1249 | It is important to note that **variables are defined on a per-version basis!** When you push a new version, the
|
1250 | existing variables from the previous version are copied, so you don't have to manually add them. However, edits
|
1251 | made to one version's environment will not affect the other versions.
|
1252 |
|
1253 | ### Defining Environment Variables
|
1254 |
|
1255 | To define an environment variable, use the `env` command:
|
1256 |
|
1257 | ```bash
|
1258 | # Will set the environment variable on Zapier.com
|
1259 | zapier env 1.0.0 MY_SECRET_VALUE 1234
|
1260 | ```
|
1261 |
|
1262 | You will likely also want to set the value locally for testing.
|
1263 |
|
1264 | ```bash
|
1265 | export MY_SECRET_VALUE=1234
|
1266 | ```
|
1267 |
|
1268 | Alternatively, we provide some extra tooling to work with an `.env` (or `.environment`, see below note) that looks like this:
|
1269 |
|
1270 | ```
|
1271 | MY_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 |
|
1276 | And then in your `test/basic.js` file:
|
1277 |
|
1278 | ```js
|
1279 | const zapier = require('zapier-platform-core');
|
1280 |
|
1281 | should('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 |
|
1295 | To view existing environment variables, use the `env` command.
|
1296 |
|
1297 | ```bash
|
1298 | # Will print a table listing the variables for this version
|
1299 | zapier env 1.0.0
|
1300 | ```
|
1301 |
|
1302 | Within 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 |
|
1304 | For example, you can access the `process.env` in your perform functions and in templates:
|
1305 |
|
1306 | ```js
|
1307 | const 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 |
|
1320 | const 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 |
|
1340 | There are two primary ways to make HTTP requests in the Zapier platform:
|
1341 |
|
1342 | 1. **Shorthand HTTP Requests** - these are simple object literals that make it easy to define simple requests.
|
1343 | 2. **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 |
|
1345 | There are also a few helper constructs you can use to reduce boilerplate:
|
1346 |
|
1347 | 1. `requestTemplate` which is an shorthand HTTP request that will be merged with every request.
|
1348 | 2. `beforeRequest` middleware which is an array of functions to mutate a request before it is sent.
|
1349 | 3. `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 |
|
1355 | For 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 |
|
1357 | This features:
|
1358 |
|
1359 | 1. Lazy `{{curly}}` replacement.
|
1360 | 2. JSON de-serialization.
|
1361 | 3. Automatic non-2xx error raising.
|
1362 |
|
1363 | ```js
|
1364 | const 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 |
|
1373 | const App = {
|
1374 | // ...
|
1375 | triggers: {
|
1376 | example: {
|
1377 | // ...
|
1378 | operation: {
|
1379 | // ...
|
1380 | perform: triggerShorthandRequest
|
1381 | }
|
1382 | }
|
1383 | }
|
1384 | };
|
1385 |
|
1386 | ```
|
1387 |
|
1388 | In 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 |
|
1390 | An 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 |
|
1394 | When 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 |
|
1396 | To make a manual HTTP request, use the `request` method of the `z` object:
|
1397 |
|
1398 | ```js
|
1399 | const 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 |
|
1420 | const 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 |
|
1437 | To POST or PUT data to your API you can do this:
|
1438 |
|
1439 | ```js
|
1440 | const 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 |
|
1478 | If 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 |
|
1482 | Try putting them in your app definition:
|
1483 |
|
1484 | ```js
|
1485 | const addHeader = (request, z, bundle) => {
|
1486 | request.headers['my-header'] = 'from zapier';
|
1487 | return request;
|
1488 | };
|
1489 |
|
1490 | const mustBe200 = (response, z, bundle) => {
|
1491 | if (response.status !== 200) {
|
1492 | throw new Error(`Unexpected status code ${response.status}`);
|
1493 | }
|
1494 | return response;
|
1495 | };
|
1496 |
|
1497 | const autoParseJson = (response, z, bundle) => {
|
1498 | response.json = z.JSON.parse(response.content);
|
1499 | return response;
|
1500 | };
|
1501 |
|
1502 | const App = {
|
1503 | // ...
|
1504 | beforeRequest: [addHeader],
|
1505 | afterResponse: [mustBe200, autoParseJson]
|
1506 | // ...
|
1507 | };
|
1508 |
|
1509 | ```
|
1510 |
|
1511 | A `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 |
|
1513 | Middleware functions can be asynchronous - just return a promise from the middleware function.
|
1514 |
|
1515 | The 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 |
|
1519 | Shorthand 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
|
1537 | z.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 |
|
1560 | The 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
|
1572 | z.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 |
|
1595 | Dehydration, 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 |
|
1602 | The 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 |
|
1609 | Here is an example that pulls in extra data for a movie:
|
1610 |
|
1611 | ```js
|
1612 | const 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 |
|
1617 | const 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 |
|
1633 | const 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 |
|
1657 | module.exports = App;
|
1658 |
|
1659 | ```
|
1660 |
|
1661 | And 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 |
|
1668 | It 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 |
|
1670 | The 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
|
1673 | const content = 'Hello world!';
|
1674 | z.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 |
|
1679 | Most likely you'd want to stream from another URL - note the usage of `z.request({raw: true})`:
|
1680 |
|
1681 | ```js
|
1682 | const fileRequest = z.request({url: 'http://example.com/file.pdf', raw: true});
|
1683 | z.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 |
|
1690 | See a full example with dehydration/hydration wired in correctly:
|
1691 |
|
1692 | ```js
|
1693 | const 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 |
|
1703 | const 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 |
|
1720 | const 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 |
|
1742 | module.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 |
|
1751 | There 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 |
|
1753 | To 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 |
|
1755 | For 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
|
1758 | zapier help logs
|
1759 | ```
|
1760 |
|
1761 | ### Console Logging
|
1762 |
|
1763 | To manually print a log statement in your code, use `z.console.log`:
|
1764 |
|
1765 | ```js
|
1766 | z.console.log('Here are the input fields', bundle.inputData);
|
1767 | ```
|
1768 |
|
1769 | The `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 |
|
1773 | To see your `z.console.log` logs, do:
|
1774 |
|
1775 | ```bash
|
1776 | zapier logs --type=console
|
1777 | ```
|
1778 |
|
1779 | ### Viewing Bundle Logs
|
1780 |
|
1781 | To see the bundle logs, do:
|
1782 |
|
1783 | ```bash
|
1784 | zapier logs --type=bundle
|
1785 | ```
|
1786 |
|
1787 | ### HTTP Logging
|
1788 |
|
1789 | If you are using the `z.request()` shortcut that we provide - HTTP logging is handled automatically for you. For example:
|
1790 |
|
1791 | ```js
|
1792 | z.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 |
|
1801 | To see the HTTP logs, do:
|
1802 |
|
1803 | ```bash
|
1804 | zapier logs --type=http
|
1805 | ```
|
1806 | To see detailed http logs including headers, request and response bodies, etc, do:
|
1807 |
|
1808 | ```bash
|
1809 | zapier logs --type=http --detailed
|
1810 | ```
|
1811 |
|
1812 |
|
1813 | ## Error Handling
|
1814 |
|
1815 | APIs are not always available. Users do not always input data correctly to
|
1816 | formulate valid requests. Thus, it is a good idea to write apps defensively and
|
1817 | plan for 4xx and 5xx responses from APIs. Without proper handling, errors often
|
1818 | have incomprehensible messages for end users, or possibly go uncaught.
|
1819 |
|
1820 | Zapier provides a couple tools to help with error handling. First is the `afterResponse`
|
1821 | middleware ([docs](#using-http-middleware)), which provides a hook for processing
|
1822 | all 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
|
1824 | various kinds of errors occur.
|
1825 |
|
1826 | ### General Errors
|
1827 |
|
1828 | Errors due to a misconfiguration in a user's Zap should be handled in your app
|
1829 | by throwing a standard JavaScript `Error` with a user-friendly message.
|
1830 | Typically, this will be prettifying 4xx responses or APIs that return errors as
|
1831 | 200s with a payload that describes the error.
|
1832 |
|
1833 | Example: `throw new Error('Your error message.');`
|
1834 |
|
1835 | A 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 |
|
1841 | Note that if a Zap raises too many error messages it will be automatically
|
1842 | turned off, so only use these if the scenario is truly an error that needs to
|
1843 | be fixed.
|
1844 |
|
1845 | ### Halting Execution
|
1846 |
|
1847 | Any operation can be interrupted or "halted" (not success, not error, but
|
1848 | stopped for some specific reason) with a `HaltedError`. You might find yourself
|
1849 | using this error in cases where a required pre-condition is not met. For instance,
|
1850 | in a create to add an email address to a list where duplicates are not allowed,
|
1851 | you would want to throw a `HaltedError` if the Zap attempted to add a duplicate.
|
1852 | This would indicate failure, but it would be treated as a soft failure.
|
1853 |
|
1854 | Unlike 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 |
|
1857 | Example: `throw new z.errors.HaltedError('Your reason.');`
|
1858 |
|
1859 | ### Stale Authentication Credentials
|
1860 |
|
1861 | For apps that require manual refresh of authorization on a regular basis, Zapier
|
1862 | provides 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
|
1865 | out informing the user to refresh the credentials.
|
1866 |
|
1867 | Example: `throw new z.errors.ExpiredAuthError('Your message.');`
|
1868 |
|
1869 | For apps that use OAuth2 + refresh or Session Auth, you can use the
|
1870 | `RefreshAuthError`. This will signal Zapier to refresh the credentials and then
|
1871 | repeat the failed operation.
|
1872 |
|
1873 | Example: `throw new z.errors.RefreshAuthError();`
|
1874 |
|
1875 |
|
1876 | ## Testing
|
1877 |
|
1878 | You can write unit tests for your Zapier app that run locally, outside of the zapier editor.
|
1879 | You can run these tests in a CI tool like [Travis](https://travis-ci.com/).
|
1880 |
|
1881 | ### Writing Unit Tests
|
1882 |
|
1883 | We 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
|
1888 | const should = require('should');
|
1889 | const 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.
|
1893 | const App = require('../index');
|
1894 | const appTester = zapier.createAppTester(App);
|
1895 |
|
1896 | describe('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 |
|
1929 | While 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
|
1932 | require('should');
|
1933 |
|
1934 | const zapier = require('zapier-platform-core');
|
1935 |
|
1936 | const App = require('../index');
|
1937 | const appTester = zapier.createAppTester(App);
|
1938 |
|
1939 | const nock = require('nock');
|
1940 |
|
1941 | describe('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 |
|
2000 | There'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 |
|
2004 | To run all your tests do:
|
2005 |
|
2006 | ```bash
|
2007 | zapier test
|
2008 | ```
|
2009 |
|
2010 | > You can also go direct with `npm test` or `node_modules/mocha/bin/mocha`.
|
2011 |
|
2012 | ### Testing & Environment Variables
|
2013 |
|
2014 | The 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
|
2017 | const zapier = require('zapier-platform-core');
|
2018 | zapier.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 |
|
2027 | Additionally, you can provide them dynamically at runtime:
|
2028 |
|
2029 | ```bash
|
2030 | CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
|
2031 | ```
|
2032 |
|
2033 | Or, `export` them explicitly and place them into the environment:
|
2034 |
|
2035 | ```bash
|
2036 | export CLIENT_ID=1234
|
2037 | export CLIENT_SECRET=abcd
|
2038 | zapier test
|
2039 | ```
|
2040 |
|
2041 |
|
2042 | ### Viewing HTTP Logs in Unit Tests
|
2043 |
|
2044 |
|
2045 | When running a unit test via `zapier test`, `z.console` statements and detailed HTTP logs print to `stdout`:
|
2046 |
|
2047 | ```bash
|
2048 | zapier test
|
2049 | ```
|
2050 |
|
2051 | Sometimes you don't want that much logging, for example in a CI test. To suppress the detailed HTTP logs do:
|
2052 |
|
2053 | ```bash
|
2054 | zapier test --quiet
|
2055 | ```
|
2056 |
|
2057 | To also suppress the HTTP summary logs do:
|
2058 |
|
2059 | ```bash
|
2060 | zapier test --very-quiet
|
2061 | ```
|
2062 |
|
2063 | ### Testing in Your CI
|
2064 |
|
2065 | Whether you use Travis, Circle, Jenkins, or anything else, we aim to make it painless to test in an automated environment.
|
2066 |
|
2067 | Behind the scenes `zapier test` is doing a pretty standard `npm test` with [mocha](https://www.npmjs.com/package/mocha) as the backend.
|
2068 |
|
2069 | This 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
|
2072 | language: node_js
|
2073 | node_js:
|
2074 | - "v8.10.0"
|
2075 | before_script: npm install -g zapier-platform-cli
|
2076 | script: CLIENT_ID=1234 CLIENT_SECRET=abcd zapier test
|
2077 | ```
|
2078 |
|
2079 | You 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 |
|
2081 | As 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 |
|
2086 | Use `npm` modules just like you would use them in any other node app, for example:
|
2087 |
|
2088 | ```bash
|
2089 | npm install --save jwt
|
2090 | ```
|
2091 |
|
2092 | And then `package.json` will be updated, and you can use them like anything else:
|
2093 |
|
2094 | ```js
|
2095 | const jwt = require('jwt');
|
2096 | ```
|
2097 |
|
2098 | During 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 |
|
2121 | We 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 |
|
2132 | And then you can have your fancy ES7 code in `src/*` and a root `index.js` like this:
|
2133 |
|
2134 | ```js
|
2135 | module.exports = require('./lib');
|
2136 | ```
|
2137 |
|
2138 | And work with commands like this:
|
2139 |
|
2140 | ```bash
|
2141 | # watch and recompile
|
2142 | npm run zapier-dev
|
2143 |
|
2144 | # tests should work fine
|
2145 | zapier test
|
2146 |
|
2147 | # every build ensures a fresh build
|
2148 | npm run zapier-push
|
2149 | ```
|
2150 |
|
2151 | There 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 |
|
2157 | See [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 |
|
2163 | We 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 |
|
2167 | Update 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 |
|
2171 | You 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 |
|
2173 | In general, use `${var}` within functions and use `{{var}}` anywhere else.
|
2174 |
|
2175 | Placeholders 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 = `a${b}c`</code>.
|
2178 |
|
2179 | ### Does Zapier support XML (SOAP) APIs?
|
2180 |
|
2181 | Not 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
|
2188 | const xml = require('pixl-xml');
|
2189 |
|
2190 | const 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 |
|
2205 | Yes, 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
|
2209 | const 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
|
2220 | const 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 |
|
2241 | module.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 |
|
2258 | If 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 |
|
2264 | const 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 |
|
2305 | To understand search-powered fields, we have to have a good understanding of dynamic dropdowns.
|
2306 |
|
2307 | When 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 |
|
2311 | The 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 |
|
2315 | 1. Some Trigger
|
2316 | 2. Calculate what day of the week it is today (Code)
|
2317 | 3. Find the spreadsheet that matches the day from Step 2
|
2318 | 4. Update the spreadsheet (with the id from step 3) with some data
|
2319 |
|
2320 | If 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 |
|
2324 | This 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 |
|
2329 | Paging 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 |
|
2331 | Paging 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 |
|
2346 | If 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 |
|
2352 | const 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 |
|
2381 | const 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 |
|
2404 | Cursors 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 |
|
2406 | Lastly, 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 |
|
2411 | Each 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 |
|
2413 | For 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 |
|
2415 | There'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 |
|
2419 | For 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 | // ...
|
2423 | let items = z.JSON.parse(response.content).items;
|
2424 | items.forEach(item => {
|
2425 | item.id = item.contactId;
|
2426 | })
|
2427 |
|
2428 | return items;
|
2429 | ```
|
2430 |
|
2431 | ## Command Line Tab Completion
|
2432 |
|
2433 | We 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 |
|
2437 | To 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
|
2441 | fpath=(~/.zsh/completion $fpath)
|
2442 |
|
2443 | # compsys initialization
|
2444 | autoload -U compinit
|
2445 | compinit
|
2446 | ```
|
2447 |
|
2448 | Next download our completion script to your completions directory:
|
2449 |
|
2450 | ```zsh
|
2451 | cd ~/.zsh/completion
|
2452 | curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/zsh/_zapier -O
|
2453 | ```
|
2454 |
|
2455 | Finally, restart your shell and start hitting TAB with the `zapier` command!
|
2456 |
|
2457 | ### Bash Completion Script
|
2458 |
|
2459 | To 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
|
2462 | cd ~/.bash_completion.d
|
2463 | curl https://raw.githubusercontent.com/zapier/zapier-platform-cli/master/goodies/bash/_zapier -O
|
2464 | ```
|
2465 |
|
2466 | Next source the script from your `~/.bash_profile`:
|
2467 |
|
2468 | ```bash
|
2469 | source ~/.bash_completion.d/_zapier
|
2470 | ```
|
2471 |
|
2472 | Finally, restart your shell and start hitting TAB with the `zapier` command!
|
2473 |
|
2474 | ## The Zapier Platform Packages
|
2475 |
|
2476 | The 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 |
|
2486 | The 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 |
|
2488 | Barring 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 |
|
2491 | The most recently released version of `cli` and `core` is `7.2.1`. You can see the versions you're working with by running `zapier -v`.
|
2492 |
|
2493 | To update `cli`, run `npm install -g zapier-platform-cli`.
|
2494 |
|
2495 | To 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 |
|
2497 | For maximum compatibility, keep the versions of `cli` and `core` in sync.
|
2498 |
|
2499 | ## Development of the CLI
|
2500 |
|
2501 | This 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 |
|
2521 | You can get help by either emailing partners@zapier.com or by joining our Slack channel https://zapier-platform-slack.herokuapp.com.
|