mirror of
https://github.com/twentyhq/twenty.git
synced 2024-08-16 17:30:24 +03:00
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:
parent
01e9545a59
commit
54735c4880
29
docs/docs/contributor/server/basics/zapier.mdx
Normal file
29
docs/docs/contributor/server/basics/zapier.mdx
Normal 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
|
@ -7,6 +7,7 @@ export {
|
||||
TbBrandFigma,
|
||||
TbBrandVscode,
|
||||
TbBrandWindows,
|
||||
TbBrandZapier,
|
||||
TbBug,
|
||||
TbBugOff,
|
||||
TbChartDots,
|
||||
@ -33,4 +34,3 @@ export {
|
||||
TbZoomQuestion,
|
||||
TbRocket
|
||||
} from "react-icons/tb";
|
||||
|
||||
|
2
packages/twenty-zapier/.env.example
Normal file
2
packages/twenty-zapier/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
SERVER_BASE_URL=http://localhost:3000
|
||||
API_KEY=YOUR_API_KEY
|
63
packages/twenty-zapier/.gitignore
vendored
Normal file
63
packages/twenty-zapier/.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/
|
||||
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
|
4
packages/twenty-zapier/.prettierrc
Normal file
4
packages/twenty-zapier/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
4
packages/twenty-zapier/.zapierapprc
Normal file
4
packages/twenty-zapier/.zapierapprc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 193409,
|
||||
"key": "App193409"
|
||||
}
|
1
packages/twenty-zapier/index.js
Normal file
1
packages/twenty-zapier/index.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./lib').default;
|
35
packages/twenty-zapier/package.json
Normal file
35
packages/twenty-zapier/package.json
Normal 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"
|
||||
}
|
||||
}
|
54
packages/twenty-zapier/src/authentication.ts
Normal file
54
packages/twenty-zapier/src/authentication.ts
Normal 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: {},
|
||||
};
|
84
packages/twenty-zapier/src/creates/create_person.ts
Normal file
84
packages/twenty-zapier/src/creates/create_person.ts
Normal 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,
|
||||
},
|
||||
};
|
11
packages/twenty-zapier/src/index.ts
Normal file
11
packages/twenty-zapier/src/index.ts
Normal 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 },
|
||||
};
|
75
packages/twenty-zapier/src/test/authentication.test.ts
Normal file
75
packages/twenty-zapier/src/test/authentication.test.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
11
packages/twenty-zapier/tsconfig.json
Normal file
11
packages/twenty-zapier/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["esnext"],
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true
|
||||
}
|
||||
}
|
2443
packages/twenty-zapier/yarn.lock
Normal file
2443
packages/twenty-zapier/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user