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:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View 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

View 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

View 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,
},
};

View 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"
}

View 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');
});
});

View 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',
},
},
};

View 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,
},
};

View 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 };