2038 zapier integration 1 initialize a zapier app with a twenty related account (#2089)

* Add doc for Zapier development

* Add twenty-zapier package

* Install zapier packages

* Update doc

* Add twenty-zapier app

* Update doc

* Update apiKey slug

* Update integration

* Update create people to person

* Update version

* Fix lint

* Remove useless comments

* Update docs

* Update version

* Update naming

* Add prettier

* Simplify docs

* Remove twenty related stuff from public doc

* Use typescript boilerplate

* Update details
This commit is contained in:
martmull 2023-10-17 21:00:20 +02:00 committed by GitHub
parent 01e9545a59
commit 54735c4880
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2842 additions and 1 deletions

View File

@ -0,0 +1,29 @@
---
title: Zapier App
sidebar_position: 3
sidebar_custom_props:
icon: TbBrandZapier
---
## Setup
- Create a [Zapier account](https://zapier.com/)
- Install [Zapier CLI](https://platform.zapier.com/quickstart/cli-tutorial) globally: `npm install -g zapier-platform-cli`
- Login with CLI with your Zapier account credentials: `zapier login`
- Install Zapier packages:
```
cd packages/twenty-zapier
yarn
```
- set environment variables:
- In **packages/twenty-zapier** launch `cp .env.example .env`
- launch local application, go to `http://localhost:3000/settings/apis` and generate an apiKey
- copy and paste the api key to replace the .env **YOUR_API_KEY** value
- `cd .. && cd twenty-zapier` to set environment variables (needs autoenv)
## Development
- Test: `yarn test`
- Lint: `yarn format`
- Watch and compile as you edit code: `yarn watch`
- Validate your Zapier app: `yarn validate`
- Deploy your Zapier app: `yarn deploy`
- List all Zapier CLI commands: `zapier`. ⚠️ make sure to run `yarn build` before any `zapier` command

View File

@ -7,6 +7,7 @@ export {
TbBrandFigma,
TbBrandVscode,
TbBrandWindows,
TbBrandZapier,
TbBug,
TbBugOff,
TbChartDots,
@ -33,4 +34,3 @@ export {
TbZoomQuestion,
TbRocket
} from "react-icons/tb";

View File

@ -0,0 +1,2 @@
SERVER_BASE_URL=http://localhost:3000
API_KEY=YOUR_API_KEY

63
packages/twenty-zapier/.gitignore vendored Normal file
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/
lib/
# 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,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -0,0 +1,4 @@
{
"id": 193409,
"key": "App193409"
}

View File

@ -0,0 +1 @@
module.exports = require('./lib').default;

View File

@ -0,0 +1,35 @@
{
"name": "twenty",
"version": "1.0.0",
"description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!",
"main": "src/index.ts",
"scripts": {
"format": "prettier . --write \"!build\"",
"test": "yarn build && jest --testTimeout 10000 --rootDir ./lib/test",
"build": "yarn clean && tsc",
"deploy": "yarn build && zapier push",
"validate": "yarn build && zapier validate",
"clean": "rimraf ./lib ./build",
"watch": "yarn clean && tsc --watch",
"_zapier-build": "yarn build"
},
"engines": {
"node": ">=v18",
"npm": ">=5.6.0"
},
"dependencies": {
"prettier": "^3.0.3",
"zapier-platform-core": "15.4.1"
},
"devDependencies": {
"@types/jest": "^29.5.5",
"@types/node": "^20.8.6",
"jest": "^29.6.0",
"rimraf": "^5.0.5",
"typescript": "^5.2.2"
},
"private": true,
"zapier": {
"convertedByCLIVersion": "15.4.1"
}
}

View File

@ -0,0 +1,54 @@
import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core';
const testAuthentication = async (z: ZObject, bundle: Bundle) => {
const options = {
url: `${process.env.SERVER_BASE_URL}/graphql`,
method: 'POST',
headers: {
Authorization: `Bearer ${bundle.authData.apiKey}`,
},
body: {
query: 'query currentWorkspace {currentWorkspace {id displayName}}',
},
} satisfies HttpRequestOptions;
return z
.request(options)
.then((response) => {
const results = response.json;
if (results.errors) {
throw new z.errors.Error(
'The API Key you supplied is incorrect',
'AuthenticationError',
results.errors,
);
}
response.throwForStatus();
return results;
})
.catch((err) => {
throw new z.errors.Error(
'The API Key you supplied is incorrect',
'AuthenticationError',
err.message,
);
});
};
export default {
type: 'custom',
test: testAuthentication,
fields: [
{
computed: false,
key: 'apiKey',
required: true,
label: 'Api Key',
type: 'string',
helpText:
'Create the api key in [your twenty workspace](https://app.twenty.com/settings/apis)',
},
],
connectionLabel: '{{data.currentWorkspace.displayName}}',
customConfig: {},
};

View File

@ -0,0 +1,84 @@
import { Bundle, ZObject } from 'zapier-platform-core';
const perform = async (z: ZObject, bundle: Bundle) => {
const response = await z.request({
body: {
query: `mutation
CreatePerson {
createOnePerson(data:{
firstName: "${bundle.inputData.firstName}",
lastName: "${bundle.inputData.lastName}",
email: "${bundle.inputData.email}",
phone: "${bundle.inputData.phone}",
city: "${bundle.inputData.city}"
}){id}}`,
},
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${bundle.authData.apiKey}`,
},
method: 'POST',
url: `${process.env.SERVER_BASE_URL}/graphql`,
});
return response.json;
};
export default {
display: {
description: 'Creates a new Person in Twenty',
hidden: false,
label: 'Create New Person',
},
key: 'create_person',
noun: 'Person',
operation: {
inputFields: [
{
key: 'firstName',
label: 'First Name',
type: 'string',
required: true,
list: false,
altersDynamicFields: false,
},
{
key: 'lastName',
label: 'Last Name',
type: 'string',
required: false,
list: false,
altersDynamicFields: false,
},
{
key: 'email',
label: 'Email',
type: 'string',
required: true,
list: false,
altersDynamicFields: false,
},
{
key: 'phone',
label: 'Phone',
type: 'string',
required: false,
list: false,
altersDynamicFields: false,
},
{
key: 'city',
label: 'City',
type: 'string',
required: false,
list: false,
altersDynamicFields: false,
},
],
sample: {
firstName: 'John',
lastName: 'Doe',
email: 'johndoe@gmail.com',
},
perform,
},
};

View File

@ -0,0 +1,11 @@
const { version } = require('../package.json');
import { version as platformVersion } from 'zapier-platform-core';
import createPerson from './creates/create_person';
import authentication from './authentication';
export default {
version,
platformVersion,
authentication: authentication,
creates: { [createPerson.key]: createPerson },
};

View File

@ -0,0 +1,75 @@
import App from '../index';
import {
Bundle,
HttpRequestOptions,
createAppTester,
tools,
ZObject,
AppError,
} from 'zapier-platform-core';
const appTester = createAppTester(App);
tools.env.inject();
const generateKey = async (z: ZObject, bundle: Bundle) => {
const options = {
url: `${process.env.SERVER_BASE_URL}/graphql`,
method: 'POST',
headers: {
Authorization: `Bearer ${bundle.authData.apiKey}`,
},
body: {
query: `mutation
CreateApiKey {
createOneApiKey(data:{
name:"${bundle.inputData.name}",
expiresAt: "${bundle.inputData.expiresAt}"
}) {token}}`,
},
} satisfies HttpRequestOptions;
return z.request(options).then((response) => {
const results = response.json;
return results.data.createOneApiKey.token;
});
};
const apiKey = String(process.env.API_KEY);
describe('custom auth', () => {
it('passes authentication and returns json', async () => {
const bundle = { authData: { apiKey } };
const response = await appTester(App.authentication.test, bundle);
expect(response.data).toHaveProperty('currentWorkspace');
expect(response.data.currentWorkspace).toHaveProperty('displayName');
});
it('fails on bad auth token format', async () => {
const bundle = { authData: { apiKey: 'bad' } };
try {
await appTester(App.authentication.test, bundle);
} catch (error: any) {
expect(error.message).toContain('The API Key you supplied is incorrect');
return;
}
throw new Error('appTester should have thrown');
});
it('fails on invalid auth token', async () => {
const bundle = {
authData: { apiKey },
inputData: { name: 'Test', expiresAt: '2020-01-01 10:10:10.000' },
};
const expiredToken = await appTester(generateKey, bundle);
const bundleWithExpiredApiKey = {
authData: { apiKey: expiredToken },
};
try {
await appTester(App.authentication.test, bundleWithExpiredApiKey);
} catch (error: any) {
expect(error.message).toContain('The API Key you supplied is incorrect');
return;
}
throw new Error('appTester should have thrown');
});
});

View File

@ -0,0 +1,25 @@
import App from '../../index';
import { createAppTester, tools } from 'zapier-platform-core';
const appTester = createAppTester(App);
tools.env.inject();
describe('creates.create_person', () => {
test('should run', async () => {
const bundle = {
authData: { apiKey: String(process.env.API_KEY) },
inputData: {
firstName: 'John',
lastName: 'Doe',
email: 'johndoe@gmail.com',
phone: '+33610203040',
city: 'Paris',
},
};
const results = await appTester(
App.creates.create_person.operation.perform,
bundle,
);
expect(results).toBeDefined();
expect(results.data?.createOnePerson?.id).toBeDefined();
});
});

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["esnext"],
"outDir": "./lib",
"rootDir": "./src",
"strict": true
}
}

File diff suppressed because it is too large Load Diff