Milestone 0: import zappier billing, Verae middleware, and Zapier research
Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
commit
b4150c8250
1364 changed files with 6814366 additions and 0 deletions
63
vendor/zapier-platform/example-apps/dynamic-dropdown/.gitignore
vendored
Normal file
63
vendor/zapier-platform/example-apps/dynamic-dropdown/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# environment variables file
|
||||
.env
|
||||
.environment
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
142
vendor/zapier-platform/example-apps/dynamic-dropdown/README.md
vendored
Normal file
142
vendor/zapier-platform/example-apps/dynamic-dropdown/README.md
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# dynamic-dropdown
|
||||
|
||||
This Zapier integration project is generated by the `zapier-platform init` CLI command.
|
||||
|
||||
These are what you normally do next:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install # or you can use pnpm or yarn
|
||||
|
||||
# Run tests
|
||||
zapier-platform test
|
||||
|
||||
# Register the integration on Zapier if you haven't
|
||||
zapier-platform register "App Title"
|
||||
|
||||
# Or you can link to an existing integration on Zapier
|
||||
zapier-platform link
|
||||
|
||||
# Push it to Zapier
|
||||
zapier-platform push
|
||||
```
|
||||
|
||||
Then, to add more features, you can use the `zapier-platform scaffold` command, for example:
|
||||
|
||||
```bash
|
||||
# Add a trigger
|
||||
zapier-platform scaffold trigger contact
|
||||
|
||||
# Add an action
|
||||
zapier-platform scaffold create contact
|
||||
```
|
||||
|
||||
Find out more on the latest docs: https://docs.zapier.com/platform
|
||||
|
||||
# dynamic-dropdown
|
||||
|
||||
This example integration demonstrates how to create **dynamic dropdowns** (also known as dynamic choices) in Zapier integrations.
|
||||
|
||||
## Dynamic Dropdown Patterns
|
||||
|
||||
There are two ways to implement dynamic dropdowns:
|
||||
|
||||
### 1. Trigger-based (Legacy Pattern)
|
||||
|
||||
Uses a separate trigger to fetch choices. Reference it with the `dynamic` property:
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'species_id',
|
||||
type: 'integer',
|
||||
label: 'Species',
|
||||
dynamic: 'species.id.name', // Format: "triggerKey.idField.labelField"
|
||||
}
|
||||
```
|
||||
|
||||
The trigger (`species`) fetches data, and Zapier uses `id` for the value and `name` for the display label.
|
||||
|
||||
### 2. Perform-based (New Pattern)
|
||||
|
||||
Uses a function to fetch choices directly. Define it with `choices.perform`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'planet_id',
|
||||
type: 'integer',
|
||||
label: 'Home Planet',
|
||||
resource: 'planet', // Explicit resource linking (see below)
|
||||
choices: {
|
||||
perform: getPlanetChoices,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Resource Linking
|
||||
|
||||
The `resource` property explicitly links an input field to a resource. This is particularly important for perform-based dropdowns since they don't have a `dynamic` property to derive the resource from.
|
||||
|
||||
```javascript
|
||||
{
|
||||
key: 'spreadsheet_id',
|
||||
resource: 'spreadsheet',
|
||||
choices: { perform: getSpreadsheets },
|
||||
}
|
||||
```
|
||||
|
||||
The perform function must return:
|
||||
|
||||
```javascript
|
||||
{
|
||||
results: [
|
||||
{ id: '1', label: 'Tatooine' },
|
||||
{ id: '2', label: 'Alderaan' },
|
||||
],
|
||||
paging_token: 'https://api.example.com/planets?page=2', // or null if no more pages
|
||||
}
|
||||
```
|
||||
|
||||
#### Pagination Support
|
||||
|
||||
The perform function receives `bundle.meta.paging_token` for subsequent page requests:
|
||||
|
||||
```javascript
|
||||
const getPlanetChoices = async (z, bundle) => {
|
||||
// First request: paging_token is undefined
|
||||
// Subsequent requests: paging_token is the value you returned previously
|
||||
const url = bundle.meta.paging_token || 'https://api.example.com/planets';
|
||||
|
||||
const response = await z.request({ url });
|
||||
|
||||
return {
|
||||
results: response.data.results.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.name,
|
||||
})),
|
||||
// Return null when there are no more pages
|
||||
paging_token: response.data.next,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## This Example
|
||||
|
||||
This integration uses the [Star Wars API](https://swapi.dev/) to demonstrate:
|
||||
|
||||
- **Species dropdown** - Trigger-based pattern using the `species` trigger
|
||||
- **Planet dropdown** - Perform-based pattern with pagination and explicit `resource` linking
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run tests
|
||||
zapier-platform test
|
||||
|
||||
# Push to Zapier
|
||||
zapier-platform push
|
||||
```
|
||||
|
||||
Find out more on the latest docs: https://docs.zapier.com/platform
|
||||
12
vendor/zapier-platform/example-apps/dynamic-dropdown/index.js
vendored
Normal file
12
vendor/zapier-platform/example-apps/dynamic-dropdown/index.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const people = require('./triggers/people');
|
||||
const species = require('./triggers/species');
|
||||
|
||||
module.exports = {
|
||||
version: require('./package.json').version,
|
||||
platformVersion: require('zapier-platform-core').version,
|
||||
|
||||
triggers: {
|
||||
[people.key]: people,
|
||||
[species.key]: species,
|
||||
},
|
||||
};
|
||||
16
vendor/zapier-platform/example-apps/dynamic-dropdown/package.json
vendored
Normal file
16
vendor/zapier-platform/example-apps/dynamic-dropdown/package.json
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "dynamic-dropdown",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"test": "jest --testTimeout 10000"
|
||||
},
|
||||
"dependencies": {
|
||||
"zapier-platform-core": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.6.0"
|
||||
},
|
||||
"private": true,
|
||||
"main": "index.js"
|
||||
}
|
||||
44
vendor/zapier-platform/example-apps/dynamic-dropdown/test/triggers.test.js
vendored
Normal file
44
vendor/zapier-platform/example-apps/dynamic-dropdown/test/triggers.test.js
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/* globals describe, expect, test */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
|
||||
const App = require('../index');
|
||||
const appTester = zapier.createAppTester(App);
|
||||
zapier.tools.env.inject();
|
||||
|
||||
describe('triggers', () => {
|
||||
test('species', async () => {
|
||||
const bundle = {
|
||||
inputData: {},
|
||||
meta: {},
|
||||
};
|
||||
|
||||
const results = await appTester(
|
||||
App.triggers.species.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(results.length).toBeGreaterThan(1);
|
||||
|
||||
const firstSpecies = results[0];
|
||||
expect(firstSpecies.id).toBe(1);
|
||||
expect(firstSpecies.name).toBe('Human');
|
||||
});
|
||||
|
||||
test('people', async () => {
|
||||
const bundle = {
|
||||
inputData: {
|
||||
species: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const results = await appTester(
|
||||
App.triggers.people.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(results.length).toBeGreaterThan(1);
|
||||
|
||||
const firstPerson = results[0];
|
||||
expect(firstPerson.id).toBe(1);
|
||||
expect(firstPerson.name).toBe('Luke Skywalker');
|
||||
});
|
||||
});
|
||||
120
vendor/zapier-platform/example-apps/dynamic-dropdown/triggers/people.js
vendored
Normal file
120
vendor/zapier-platform/example-apps/dynamic-dropdown/triggers/people.js
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
const { extractID } = require('../utils');
|
||||
|
||||
/**
|
||||
* PERFORM-BASED choices WITH PAGINATION (NEW pattern)
|
||||
* Fetches planets from the Star Wars API with pagination support.
|
||||
*
|
||||
* - bundle.meta.paging_token is a full URL from the previous response
|
||||
* - Return paging_token as the API's next page URL (or null if no more pages)
|
||||
*
|
||||
* MUST return: { results: [...], paging_token: string|null }
|
||||
*/
|
||||
const getPlanetChoices = async (z, bundle) => {
|
||||
// paging_token is a full URL to the next page (from SWAPI's "next" field)
|
||||
// First page: paging_token is undefined/null, use default URL
|
||||
const url = bundle.meta.paging_token || 'https://swapi.dev/api/planets/';
|
||||
|
||||
const response = await z.request({ url });
|
||||
const data = response.data;
|
||||
|
||||
// SWAPI returns: { results: [...], next: "url" or null }
|
||||
return {
|
||||
results: data.results.map((planet) => ({
|
||||
id: extractID(planet.url),
|
||||
label: planet.name,
|
||||
})),
|
||||
// Return SWAPI's next URL as our paging_token
|
||||
paging_token: data.next,
|
||||
};
|
||||
};
|
||||
|
||||
// Fetches a list of records from the endpoint
|
||||
const perform = async (z, bundle) => {
|
||||
// Ideally, we should poll through all the pages of results, but in this
|
||||
// example we're going to omit that part. Thus, this trigger only "see" the
|
||||
// people in their first page of results.
|
||||
const response = await z.request({ url: 'https://swapi.info/api/people/' });
|
||||
let peopleArray = response.data;
|
||||
|
||||
if (bundle.inputData.species_id) {
|
||||
// The Zap's setup has requested a specific species of person. Since the
|
||||
// API/endpoint can't perform the filtering, we'll perform it here, within
|
||||
// the integration, and return the matching objects/records back to Zapier.
|
||||
peopleArray = peopleArray.filter((person) => {
|
||||
let speciesID;
|
||||
if (!person.species || !person.species.length) {
|
||||
speciesID = 1; // Assume human if species is not provided
|
||||
} else {
|
||||
speciesID = extractID(person.species[0]);
|
||||
}
|
||||
return speciesID === bundle.inputData.species_id;
|
||||
});
|
||||
}
|
||||
|
||||
if (bundle.inputData.planet_id) {
|
||||
// The Zap's setup has requested a specific home planet. Filter people by
|
||||
// homeworld (SWAPI people have a homeworld URL).
|
||||
peopleArray = peopleArray.filter((person) => {
|
||||
if (!person.homeworld) return false;
|
||||
const homeworldID = extractID(person.homeworld);
|
||||
return homeworldID === bundle.inputData.planet_id;
|
||||
});
|
||||
}
|
||||
|
||||
return peopleArray.map((person) => {
|
||||
person.id = extractID(person.url);
|
||||
return person;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'people',
|
||||
noun: 'person',
|
||||
display: {
|
||||
label: 'New Person',
|
||||
description: 'Triggers when a new person is added.',
|
||||
},
|
||||
|
||||
operation: {
|
||||
inputFields: [
|
||||
// TRIGGER-BASED dynamic dropdown (legacy pattern)
|
||||
// Uses a separate trigger to fetch choices
|
||||
{
|
||||
key: 'species_id',
|
||||
type: 'integer',
|
||||
label: 'Species (trigger-based)',
|
||||
helpText:
|
||||
'Filter by species. Uses trigger-based dynamic dropdown (dynamic: "species.id.name").',
|
||||
dynamic: 'species.id.name',
|
||||
altersDynamicFields: true,
|
||||
},
|
||||
// PERFORM-BASED dynamic dropdown WITH PAGINATION (new pattern)
|
||||
// Uses a function to fetch choices directly
|
||||
{
|
||||
key: 'planet_id',
|
||||
type: 'integer',
|
||||
label: 'Home Planet (perform-based)',
|
||||
helpText:
|
||||
'Filter by home planet. Uses perform-based dynamic dropdown with pagination support.',
|
||||
resource: 'planet', // Explicit resource linking for perform-based dropdowns
|
||||
choices: {
|
||||
perform: getPlanetChoices,
|
||||
},
|
||||
},
|
||||
],
|
||||
perform,
|
||||
sample: {
|
||||
id: '1',
|
||||
name: 'Luke Skywalker',
|
||||
birth_year: '19 BBY',
|
||||
eye_color: 'Blue',
|
||||
gender: 'Male',
|
||||
hair_color: 'Blond',
|
||||
height: '172',
|
||||
mass: '77',
|
||||
skin_color: 'Fair',
|
||||
created: '2014-12-09T13:50:51.644000Z',
|
||||
edited: '2014-12-10T13:52:43.172000Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
41
vendor/zapier-platform/example-apps/dynamic-dropdown/triggers/species.js
vendored
Normal file
41
vendor/zapier-platform/example-apps/dynamic-dropdown/triggers/species.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const { extractID } = require('../utils');
|
||||
|
||||
// Fetches a list of records from the endpoint
|
||||
const perform = async (z, bundle) => {
|
||||
const request = {
|
||||
url: 'https://swapi.info/api/species/',
|
||||
params: {},
|
||||
};
|
||||
|
||||
// This API returns things in "pages" of results
|
||||
if (bundle.meta.page) {
|
||||
request.params.page = 1 + bundle.meta.page;
|
||||
}
|
||||
|
||||
const response = await z.request(request);
|
||||
const speciesArray = response.data;
|
||||
return speciesArray.map((species) => {
|
||||
species.id = extractID(species.url);
|
||||
return species;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
key: 'species',
|
||||
noun: 'Species',
|
||||
display: {
|
||||
label: 'List of Species',
|
||||
description:
|
||||
'This is a hidden trigger, and is used in a Dynamic Dropdown of another trigger.',
|
||||
hidden: true,
|
||||
},
|
||||
|
||||
operation: {
|
||||
// Since this is a "hidden" trigger, there aren't any inputFields needed
|
||||
perform,
|
||||
// The folowing is a "hint" to the Zap Editor that this trigger returns data
|
||||
// "in pages", and that the UI should display an option to "load more" to
|
||||
// the human.
|
||||
canPaginate: true,
|
||||
},
|
||||
};
|
||||
12
vendor/zapier-platform/example-apps/dynamic-dropdown/utils.js
vendored
Normal file
12
vendor/zapier-platform/example-apps/dynamic-dropdown/utils.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Some handy stuff that's used in various places
|
||||
|
||||
// Extract the numeric ID from a URL like 'https://swapi.dev/api/people/1/'
|
||||
const extractID = (urlString) => {
|
||||
const match = urlString.match(/\/(\d+)\/?$/);
|
||||
if (match) {
|
||||
return parseInt(match[1]);
|
||||
}
|
||||
throw new Error(`ID not found in URL: ${urlString}`);
|
||||
};
|
||||
|
||||
module.exports = { extractID };
|
||||
Loading…
Add table
Add a link
Reference in a new issue