2114 timebox make sure the zapier integrations supports custom objects (#3091)
* Fix build command * Add hidden trigger to fetch object names * Remove useless actions * Rename createObject to createRecord
This commit is contained in:
@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"prettier": "^3.0.3",
|
||||
"zapier-platform-core": "15.4.1"
|
||||
"zapier-platform-core": "15.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.5",
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import { Bundle, ZObject } from 'zapier-platform-core';
|
||||
import handleQueryParams from '../utils/handleQueryParams';
|
||||
import requestDb from '../utils/requestDb';
|
||||
|
||||
const perform = async (z: ZObject, bundle: Bundle) => {
|
||||
const query = `
|
||||
mutation createCompany {
|
||||
createCompany(
|
||||
data:{${handleQueryParams(bundle.inputData)}}
|
||||
)
|
||||
{id}
|
||||
}`;
|
||||
return await requestDb(z, bundle, query);
|
||||
};
|
||||
export default {
|
||||
display: {
|
||||
description: 'Creates a new Company in Twenty',
|
||||
hidden: false,
|
||||
label: 'Create New Company',
|
||||
},
|
||||
key: 'create_company',
|
||||
noun: 'Company',
|
||||
operation: {
|
||||
inputFields: [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Company Name',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'address',
|
||||
label: 'Address',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'domainName',
|
||||
label: 'Url',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'linkedinLink__url',
|
||||
label: 'Linkedin Link Url',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'linkedinLink__label',
|
||||
label: 'Linkedin Link Label',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'xLink__url',
|
||||
label: 'Twitter Link Url',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'xLink__label',
|
||||
label: 'Twitter Link Label',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'annualRecurringRevenue__amountMicros',
|
||||
label: 'ARR (Annual Recurring Revenue) amount micros',
|
||||
type: 'number',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'annualRecurringRevenue__currencyCode',
|
||||
label: 'ARR (Annual Recurring Revenue) currency Code',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'idealCustomerProfile',
|
||||
label: 'ICP (Ideal Customer Profile)',
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'employees',
|
||||
label: 'Number of Employees',
|
||||
type: 'number',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
],
|
||||
sample: {
|
||||
name: 'Apple',
|
||||
address: 'apple.com',
|
||||
domainName: 'Cupertino',
|
||||
linkedinUrl__url: '/apple',
|
||||
linkedinUrl__label: 'Apple',
|
||||
xUrl__url: '/apple',
|
||||
xUrl__label: 'Apple',
|
||||
annualRecurringRevenue__amountMicros: 1000000000,
|
||||
annualRecurringRevenue__currencyCode: 'USD',
|
||||
idealCustomerProfile: true,
|
||||
employees: 10000,
|
||||
},
|
||||
perform,
|
||||
},
|
||||
};
|
||||
@ -1,75 +0,0 @@
|
||||
import { Bundle, ZObject } from 'zapier-platform-core';
|
||||
import handleQueryParams from '../utils/handleQueryParams';
|
||||
import requestDb from '../utils/requestDb';
|
||||
|
||||
const perform = async (z: ZObject, bundle: Bundle) => {
|
||||
const query = `
|
||||
mutation createPerson {
|
||||
createPerson(
|
||||
data:{${handleQueryParams(bundle.inputData)}}
|
||||
)
|
||||
{id}
|
||||
}`;
|
||||
return await requestDb(z, bundle, query);
|
||||
};
|
||||
export default {
|
||||
display: {
|
||||
description: 'Creates a new Person in Twenty',
|
||||
hidden: false,
|
||||
label: 'Create New Person',
|
||||
},
|
||||
key: 'create_person',
|
||||
noun: 'Person',
|
||||
operation: {
|
||||
inputFields: [
|
||||
{
|
||||
key: 'name__firstName',
|
||||
label: 'First Name',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'name__lastName',
|
||||
label: 'Last Name',
|
||||
type: 'string',
|
||||
required: false,
|
||||
list: false,
|
||||
altersDynamicFields: false,
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
type: 'string',
|
||||
required: false,
|
||||
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: {
|
||||
name__firstName: 'John',
|
||||
name__lastName: 'Doe',
|
||||
email: 'johndoe@gmail.com',
|
||||
phone: '0390900909',
|
||||
city: 'Paris',
|
||||
},
|
||||
perform,
|
||||
},
|
||||
};
|
||||
52
packages/twenty-zapier/src/creates/create_record.ts
Normal file
52
packages/twenty-zapier/src/creates/create_record.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Bundle, ZObject } from "zapier-platform-core";
|
||||
import requestDb, { requestSchema } from "../utils/requestDb";
|
||||
import handleQueryParams from "../utils/handleQueryParams";
|
||||
import { capitalize } from "../utils/capitalize";
|
||||
import { computeInputFields } from "../utils/computeInputFields";
|
||||
|
||||
const recordInputFields = async (z: ZObject, bundle: Bundle) => {
|
||||
const schema = await requestSchema(z, bundle)
|
||||
const infos = schema.components.schemas[bundle.inputData.nameSingular]
|
||||
|
||||
return computeInputFields(infos);
|
||||
}
|
||||
|
||||
const perform = async (z: ZObject, bundle: Bundle) => {
|
||||
const data = bundle.inputData
|
||||
const nameSingular = data.nameSingular
|
||||
delete data.nameSingular
|
||||
const query = `
|
||||
mutation create${capitalize(nameSingular)} {
|
||||
create${capitalize(nameSingular)}(
|
||||
data:{${handleQueryParams(data)}}
|
||||
)
|
||||
{id}
|
||||
}`;
|
||||
return await requestDb(z, bundle, query);
|
||||
};
|
||||
|
||||
export default {
|
||||
display: {
|
||||
description: 'Creates a new Record in Twenty',
|
||||
hidden: false,
|
||||
label: 'Create New Record',
|
||||
},
|
||||
key: 'create_record',
|
||||
noun: 'Record',
|
||||
operation: {
|
||||
inputFields: [
|
||||
{
|
||||
key: 'nameSingular',
|
||||
required: true,
|
||||
label: 'Name of the Record to create',
|
||||
dynamic: 'find_objects.nameSingular',
|
||||
altersDynamicFields: true,
|
||||
},
|
||||
recordInputFields
|
||||
],
|
||||
sample: {
|
||||
id: '179ed459-79cf-41d9-ab85-96397fa8e936',
|
||||
},
|
||||
perform
|
||||
},
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
const { version } = require('../package.json');
|
||||
import { version as platformVersion } from 'zapier-platform-core';
|
||||
import createPerson from './creates/create_person';
|
||||
import createCompany from './creates/create_company';
|
||||
import createRecord from './creates/create_record';
|
||||
import findObjects from './triggers/find_objects'
|
||||
import authentication from './authentication';
|
||||
import 'dotenv/config';
|
||||
|
||||
@ -9,8 +9,10 @@ export default {
|
||||
version,
|
||||
platformVersion,
|
||||
authentication: authentication,
|
||||
triggers: {
|
||||
[findObjects.key]: findObjects,
|
||||
},
|
||||
creates: {
|
||||
[createPerson.key]: createPerson,
|
||||
[createCompany.key]: createCompany,
|
||||
[createRecord.key]: createRecord,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import App from '../../index';
|
||||
import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core';
|
||||
import getBundle from '../../utils/getBundle';
|
||||
import requestDb from '../../utils/requestDb';
|
||||
const appTester = createAppTester(App);
|
||||
tools.env.inject();
|
||||
|
||||
describe('creates.create_person', () => {
|
||||
test('should run', async () => {
|
||||
const bundle = getBundle({
|
||||
name: {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?.createPerson?.id).toBeDefined();
|
||||
const checkDbResult = await appTester(
|
||||
(z: ZObject, bundle: Bundle) =>
|
||||
requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`query findPerson {person(filter: {id: {eq: "${results.data.createPerson.id}"}}){phone}}`,
|
||||
),
|
||||
bundle,
|
||||
);
|
||||
expect(checkDbResult.data.person.phone).toEqual('+33610203040');
|
||||
});
|
||||
|
||||
test('should run with not required params', async () => {
|
||||
const bundle = getBundle({});
|
||||
const results = await appTester(
|
||||
App.creates.create_person.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(results).toBeDefined();
|
||||
expect(results.data?.createPerson?.id).toBeDefined();
|
||||
const checkDbResult = await appTester(
|
||||
(z: ZObject, bundle: Bundle) =>
|
||||
requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`query findPerson {person(filter: {id: {eq: "${results.data.createPerson.id}"}}){phone}}`,
|
||||
),
|
||||
bundle,
|
||||
);
|
||||
expect(checkDbResult.data.person.phone).toEqual("");
|
||||
});
|
||||
});
|
||||
@ -1,13 +1,14 @@
|
||||
import App from '../../index';
|
||||
import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core';
|
||||
import getBundle from '../../utils/getBundle';
|
||||
import requestDb from '../../utils/requestDb';
|
||||
import getBundle from "../../utils/getBundle";
|
||||
import { Bundle, createAppTester, tools, ZObject } from "zapier-platform-core";
|
||||
import requestDb from "../../utils/requestDb";
|
||||
const appTester = createAppTester(App);
|
||||
tools.env.inject;
|
||||
|
||||
describe('creates.create_company', () => {
|
||||
test('should run', async () => {
|
||||
describe('creates.create_record', () => {
|
||||
test('should run to create a Company Record', async () => {
|
||||
const bundle = getBundle({
|
||||
nameSingular: 'Company',
|
||||
name: 'Company Name',
|
||||
address: 'Company Address',
|
||||
domainName: 'Company Domain Name',
|
||||
@ -18,7 +19,7 @@ describe('creates.create_company', () => {
|
||||
employees: 25,
|
||||
});
|
||||
const result = await appTester(
|
||||
App.creates.create_company.operation.perform,
|
||||
App.creates.create_record.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
@ -35,26 +36,30 @@ describe('creates.create_company', () => {
|
||||
expect(checkDbResult.data.company.annualRecurringRevenue.amountMicros).toEqual(
|
||||
100000000000,
|
||||
);
|
||||
});
|
||||
test('should run with not required params', async () => {
|
||||
const bundle = getBundle({});
|
||||
})
|
||||
test('should run to create a Person Record', async () => {
|
||||
const bundle = getBundle({
|
||||
nameSingular: 'Person',
|
||||
name: {firstName: 'John', lastName: 'Doe'},
|
||||
email: 'johndoe@gmail.com',
|
||||
phone: '+33610203040',
|
||||
city: 'Paris',
|
||||
});
|
||||
const result = await appTester(
|
||||
App.creates.create_company.operation.perform,
|
||||
App.creates.create_record.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.data?.createCompany?.id).toBeDefined();
|
||||
expect(result.data?.createPerson?.id).toBeDefined();
|
||||
const checkDbResult = await appTester(
|
||||
(z: ZObject, bundle: Bundle) =>
|
||||
requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`query findCompany {company(filter: {id: {eq: "${result.data.createCompany.id}"}}){id annualRecurringRevenue{amountMicros currencyCode}}}`,
|
||||
`query findPerson {person(filter: {id: {eq: "${result.data.createPerson.id}"}}){phone}}`,
|
||||
),
|
||||
bundle,
|
||||
);
|
||||
expect(checkDbResult.data.company.annualRecurringRevenue.amountMicros).toEqual(
|
||||
null,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(checkDbResult.data.person.phone).toEqual('+33610203040');
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,17 @@
|
||||
import { createAppTester } from "zapier-platform-core";
|
||||
import getBundle from '../../utils/getBundle';
|
||||
import App from '../../index';
|
||||
|
||||
const appTester = createAppTester(App);
|
||||
describe('triggers.find_objects', () => {
|
||||
test('should run', async () => {
|
||||
const bundle = getBundle({});
|
||||
const result = await appTester(
|
||||
App.triggers.find_objects.operation.perform,
|
||||
bundle,
|
||||
);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.length).toBeGreaterThan(1)
|
||||
expect(result[0].nameSingular).toBeDefined()
|
||||
})
|
||||
})
|
||||
8
packages/twenty-zapier/src/test/utils/capitalize.spec.ts
Normal file
8
packages/twenty-zapier/src/test/utils/capitalize.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { capitalize } from "../../utils/capitalize";
|
||||
|
||||
describe('capitalize', ()=> {
|
||||
test('should capitalize properly', ()=> {
|
||||
expect(capitalize('word')).toEqual('Word')
|
||||
expect(capitalize('word word')).toEqual('Word word')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,42 @@
|
||||
import { computeInputFields } from "../../utils/computeInputFields";
|
||||
|
||||
describe('computeInputFields', ()=> {
|
||||
test('should create Person input fields properly', ()=> {
|
||||
const personInfos = {
|
||||
type: "object",
|
||||
properties: {
|
||||
email: {
|
||||
type: "string"
|
||||
},
|
||||
xLink: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: {
|
||||
type: "string"
|
||||
},
|
||||
label: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
avatarUrl: {
|
||||
type: "string"
|
||||
},
|
||||
favorites: {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: "#/components/schemas/Favorite"
|
||||
}
|
||||
},
|
||||
},
|
||||
example: {},
|
||||
required: ['avatarUrl']
|
||||
}
|
||||
expect(computeInputFields(personInfos)).toEqual([
|
||||
{ key: "email", label: "Email", required: false, type: "string" },
|
||||
{ key: "xLink__url", label: "X Link: Url", required: false, type: "string" },
|
||||
{ key: "xLink__label", label: "X Link: Label", required: false, type: "string" },
|
||||
{ key: "avatarUrl", label: "Avatar Url", required: true, type: "string" },
|
||||
])
|
||||
})
|
||||
})
|
||||
7
packages/twenty-zapier/src/test/utils/labelize.spec.ts
Normal file
7
packages/twenty-zapier/src/test/utils/labelize.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { labelling } from "../../utils/labelling";
|
||||
|
||||
describe('labelling', ()=> {
|
||||
test('should label properly', ()=> {
|
||||
expect(labelling('createdAt')).toEqual('Created At')
|
||||
})
|
||||
})
|
||||
22
packages/twenty-zapier/src/triggers/find_objects.ts
Normal file
22
packages/twenty-zapier/src/triggers/find_objects.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Bundle, ZObject } from "zapier-platform-core";
|
||||
import { requestSchema } from "../utils/requestDb";
|
||||
|
||||
const objectListRequest = async (z: ZObject, bundle: Bundle) => {
|
||||
const schema = await requestSchema(z, bundle)
|
||||
return Object.keys(schema.components.schemas).map((schema)=> {
|
||||
return {id: schema, nameSingular: schema}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
display: {
|
||||
description: 'Find objects',
|
||||
label: 'Find objects',
|
||||
hidden: true,
|
||||
},
|
||||
key: 'find_objects',
|
||||
noun: 'Object',
|
||||
operation: {
|
||||
perform: objectListRequest,
|
||||
},
|
||||
}
|
||||
3
packages/twenty-zapier/src/utils/capitalize.ts
Normal file
3
packages/twenty-zapier/src/utils/capitalize.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const capitalize = (word: string): string => {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
}
|
||||
54
packages/twenty-zapier/src/utils/computeInputFields.ts
Normal file
54
packages/twenty-zapier/src/utils/computeInputFields.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { labelling } from "../utils/labelling";
|
||||
|
||||
type Infos = {
|
||||
properties: {
|
||||
[field: string]: {
|
||||
type: string;
|
||||
properties?: { [field: string]: { type: string } }
|
||||
items?: { [$ref: string]: string }
|
||||
}
|
||||
},
|
||||
example: object,
|
||||
required: string[]
|
||||
}
|
||||
|
||||
export const computeInputFields = (infos: Infos): object[] => {
|
||||
const result = []
|
||||
|
||||
for (const fieldName of Object.keys(infos.properties)) {
|
||||
switch (infos.properties[fieldName].type) {
|
||||
case 'array':
|
||||
break;
|
||||
case 'object':
|
||||
if (!infos.properties[fieldName].properties) {
|
||||
break;
|
||||
}
|
||||
for (const subFieldName of Object.keys(infos.properties[fieldName].properties || {})) {
|
||||
const field = {
|
||||
key: `${fieldName}__${subFieldName}`,
|
||||
label: `${labelling(fieldName)}: ${labelling(subFieldName)}`,
|
||||
type: infos.properties[fieldName].properties?.[subFieldName].type,
|
||||
required: false,
|
||||
}
|
||||
if (infos.required?.includes(fieldName)) {
|
||||
field.required = true
|
||||
}
|
||||
result.push(field)
|
||||
}
|
||||
break;
|
||||
default:
|
||||
const field = {
|
||||
key: fieldName,
|
||||
label: labelling(fieldName),
|
||||
type: infos.properties[fieldName].type,
|
||||
required: false,
|
||||
}
|
||||
if (infos.required?.includes(fieldName)) {
|
||||
field.required = true
|
||||
}
|
||||
result.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
9
packages/twenty-zapier/src/utils/labelling.ts
Normal file
9
packages/twenty-zapier/src/utils/labelling.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { capitalize } from "../utils/capitalize";
|
||||
|
||||
export const labelling = (str: string): string => {
|
||||
return str
|
||||
.replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`)
|
||||
.split(' ')
|
||||
.map((word)=> capitalize(word))
|
||||
.join(' ');
|
||||
}
|
||||
@ -1,5 +1,20 @@
|
||||
import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core';
|
||||
|
||||
export const requestSchema = async (z: ZObject, bundle: Bundle) => {
|
||||
const options = {
|
||||
url: `${process.env.SERVER_BASE_URL}/open-api`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${bundle.authData.apiKey}`,
|
||||
},
|
||||
} satisfies HttpRequestOptions;
|
||||
|
||||
return z.request(options)
|
||||
.then((response) => response.json)
|
||||
}
|
||||
|
||||
const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
|
||||
const options = {
|
||||
url: `${process.env.SERVER_BASE_URL}/graphql`,
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
"lib": ["esnext"],
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user