UNPKG

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