Zapier integration (#491)

* create zapier app

* install sanctum

* move OAuthProviderController

* make `api-external` middleware

* add zapier endpoints

* add tests

* token management

* zapier event handler

* add policy

* use `slug` instead of `id`

* wip

* check policies

* change api prefix to `external`

* ui tweaks

* validate token abilities

* open zapier URL

* zapier ui tweaks

* update zap

* Fix linting

* Added sample endpoints + minor UI changes

* Run PHP code linter

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Boris Lepikhin
2024-08-12 02:14:02 -07:00
committed by GitHub
parent 7ad62fb3ea
commit 517bccc695
61 changed files with 5799 additions and 51 deletions

65
integrations/zapier/.gitignore vendored Normal file
View File

@@ -0,0 +1,65 @@
# 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/
# 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
# zapier app config
.zapierapprc

View File

@@ -0,0 +1,45 @@
# Zapier
Install Zapier
```
npm install -g zapier-platform-cli
```
Install dependencies
```
cd `zapier`
npm install
```
Login to Zapier
```
zapier login
```
Register the app
```
zapier register [TITLE]
```
Publish the app
```
zapier push
```
Set the base URL to receive webhooks from Zapier. The version usually looks like 1.0.0.
```
zapier env:set [VERSION] BASE_URL=[BASE_URL]
```
## Testing
- Create an access token: http://localhost:3000/settings/access-tokens
- Create a Zap
- Authenticate using your token
- Submit a form

20
integrations/zapier/authentication.js vendored Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
type: 'custom',
test: {
removeMissingValuesFrom: { body: false, params: false },
url: '{{process.env.BASE_URL}}/external/zapier/validate',
},
fields: [
{
helpText:
'Enter your API key, located at https://opnform.com/settings/access-tokens',
computed: false,
key: 'api_key',
required: true,
label: 'API Key',
type: 'string',
},
],
connectionLabel: '{{bundle.inputData.name}} {{bundle.inputData.email}}',
customConfig: {},
};

24
integrations/zapier/index.js vendored Normal file
View File

@@ -0,0 +1,24 @@
const authentication = require('./authentication');
const newSubmissionTrigger = require('./triggers/new_submission.js');
const listWorkspacesTrigger = require('./triggers/list_workspaces.js');
const listFormsTrigger = require('./triggers/list_forms.js');
module.exports = {
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
requestTemplate: {
headers: {
Authorization: 'Bearer {{bundle.authData.api_key}}',
'X-API-KEY': '{{bundle.authData.api_key}}',
},
params: { api_key: '{{bundle.authData.api_key}}' },
body: {},
},
authentication: authentication,
searches: {},
triggers: {
[newSubmissionTrigger.key]: newSubmissionTrigger,
[listWorkspacesTrigger.key]: listWorkspacesTrigger,
[listFormsTrigger.key]: listFormsTrigger,
},
};

3799
integrations/zapier/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
{
"name": "opnform-test-1",
"version": "1.0.1",
"description": "opnform-test-1",
"main": "index.js",
"scripts": {
"test": "jest --testTimeout 10000"
},
"engines": {
"node": ">=v18",
"npm": ">=5.6.0"
},
"dependencies": {
"zapier-platform-core": "15.8.0"
},
"devDependencies": {
"jest": "^29.6.0"
},
"private": true,
"zapier": {
"convertedByCLIVersion": "15.8.0"
}
}

View File

@@ -0,0 +1,20 @@
const zapier = require('zapier-platform-core');
// Use this to make test calls into your app:
const App = require('../../index');
const appTester = zapier.createAppTester(App);
// read the `.env` file into the environment, if available
zapier.tools.env.inject();
describe('triggers.list_forms', () => {
it('should run', async () => {
const bundle = { inputData: {} };
const results = await appTester(
App.triggers['list_forms'].operation.perform,
bundle
);
expect(results).toBeDefined();
// TODO: add more assertions
});
});

View File

@@ -0,0 +1,20 @@
const zapier = require('zapier-platform-core');
// Use this to make test calls into your app:
const App = require('../../index');
const appTester = zapier.createAppTester(App);
// read the `.env` file into the environment, if available
zapier.tools.env.inject();
describe('triggers.list_workspaces', () => {
it('should run', async () => {
const bundle = { inputData: {} };
const results = await appTester(
App.triggers['list_workspaces'].operation.perform,
bundle
);
expect(results).toBeDefined();
// TODO: add more assertions
});
});

View File

@@ -0,0 +1,20 @@
const zapier = require('zapier-platform-core');
// Use this to make test calls into your app:
const App = require('../../index');
const appTester = zapier.createAppTester(App);
// read the `.env` file into the environment, if available
zapier.tools.env.inject();
describe('triggers.new_submission', () => {
it('should run', async () => {
const bundle = { inputData: {} };
const results = await appTester(
App.triggers['new_submission'].operation.perform,
bundle
);
expect(results).toBeDefined();
// TODO: add more assertions
});
});

View File

@@ -0,0 +1,37 @@
module.exports = {
operation: {
perform: {
headers: { Accept: 'application/json' },
params: {
api_key: '{{bundle.authData.api_key}}',
workspace_id: '{{bundle.inputData.workspace_id}}',
},
removeMissingValuesFrom: { body: false, params: false },
url: '{{process.env.BASE_URL}}/external/zapier/forms',
},
inputFields: [
{
key: 'workspace_id',
type: 'string',
dynamic: 'list_workspaces.id.name',
label: 'Workspace',
required: true,
list: false,
altersDynamicFields: false,
},
],
sample: { id: 'my-form', name: 'My Form' },
outputFields: [
{ key: 'id', label: 'ID', type: 'string' },
{ key: 'name', label: 'Name', type: 'string' },
],
canPaginate: false,
},
display: {
description: 'Get the list of all forms',
hidden: true,
label: 'List Forms',
},
key: 'list_forms',
noun: 'Form',
};

View File

@@ -0,0 +1,21 @@
module.exports = {
operation: {
perform: {
headers: { Accept: 'application/json' },
removeMissingValuesFrom: { body: false, params: false },
url: '{{process.env.BASE_URL}}/external/zapier/workspaces',
},
sample: { id: 1, name: 'My Workspace' },
outputFields: [
{ key: 'id', label: 'ID', type: 'integer' },
{ key: 'name', label: 'Name', type: 'string' },
],
},
display: {
description: "Get the list of all user's workspaces",
hidden: true,
label: 'List Workspaces',
},
key: 'list_workspaces',
noun: 'Workspace',
};

View File

@@ -0,0 +1,81 @@
const perform = async (z, bundle) => {
return [bundle.cleanedRequest];
};
const performList = async (z, bundle) => {
// Replace with the actual URL that returns recent submissions
const response = await z.request({
url: `${process.env.BASE_URL}/external/zapier/submissions/recent`,
params: {
form_id: bundle.inputData.form_id,
},
});
// Ensure the structure of the response matches the webhook data structure
return response.data;
};
module.exports = {
operation: {
perform: perform,
performList: performList,
sample: {
"form_title": "Your form title",
"form_slug": "your-form-slug-og4lhg"
},
inputFields: [
{
key: 'workspace_id',
type: 'string',
label: 'Workspace',
dynamic: 'list_workspaces.id.name',
required: true,
list: false,
altersDynamicFields: true,
},
{
key: 'form_id',
type: 'string',
label: 'Form',
dynamic: 'list_forms.id.name',
required: true,
list: false,
altersDynamicFields: false,
},
],
type: 'hook',
performUnsubscribe: {
body: {
hookUrl: '{{bundle.subscribeData.id}}',
form_id: '{{bundle.inputData.form_id}}',
},
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
method: 'DELETE',
removeMissingValuesFrom: { body: false, params: false },
url: '{{process.env.BASE_URL}}/external/zapier/webhook',
},
performSubscribe: {
body: {
hookUrl: '{{bundle.targetUrl}}',
form_id: '{{bundle.inputData.form_id}}',
},
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
method: 'POST',
removeMissingValuesFrom: { body: false, params: false },
url: '{{process.env.BASE_URL}}/external/zapier/webhook',
},
},
display: {
description: 'Triggers when a new submission is created.',
hidden: false,
label: 'New Submission',
},
key: 'new_submission',
noun: 'Submission',
};