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:
martmull 2023-12-20 18:41:30 +01:00 committed by GitHub
parent 984fc76b94
commit b1841d0e2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 273 additions and 292 deletions

View File

@ -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",

View File

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

View File

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

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

View File

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

View File

@ -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("");
});
});

View File

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

View File

@ -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()
})
})

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

View File

@ -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" },
])
})
})

View File

@ -0,0 +1,7 @@
import { labelling } from "../../utils/labelling";
describe('labelling', ()=> {
test('should label properly', ()=> {
expect(labelling('createdAt')).toEqual('Created At')
})
})

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

View File

@ -0,0 +1,3 @@
export const capitalize = (word: string): string => {
return word.charAt(0).toUpperCase() + word.slice(1)
}

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

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

View File

@ -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`,

View File

@ -6,6 +6,8 @@
"lib": ["esnext"],
"outDir": "./lib",
"rootDir": "./src",
"strict": true
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}

View File

@ -15190,7 +15190,7 @@ __metadata:
languageName: node
linkType: hard
"@zapier/secret-scrubber@npm:^1.0.7":
"@zapier/secret-scrubber@npm:^1.0.8":
version: 1.0.8
resolution: "@zapier/secret-scrubber@npm:1.0.8"
dependencies:
@ -41285,7 +41285,7 @@ __metadata:
rimraf: "npm:^5.0.5"
typescript: "npm:^5.2.2"
zapier-platform-cli: "npm:^15.4.1"
zapier-platform-core: "npm:15.4.1"
zapier-platform-core: "npm:15.5.1"
languageName: unknown
linkType: soft
@ -44005,12 +44005,12 @@ __metadata:
languageName: node
linkType: hard
"zapier-platform-core@npm:15.4.1":
version: 15.4.1
resolution: "zapier-platform-core@npm:15.4.1"
"zapier-platform-core@npm:15.5.1":
version: 15.5.1
resolution: "zapier-platform-core@npm:15.5.1"
dependencies:
"@types/node": "npm:^20.3.1"
"@zapier/secret-scrubber": "npm:^1.0.7"
"@zapier/secret-scrubber": "npm:^1.0.8"
bluebird: "npm:3.7.2"
content-disposition: "npm:0.5.4"
dotenv: "npm:12.0.4 "
@ -44021,21 +44021,21 @@ __metadata:
node-fetch: "npm:2.6.7"
oauth-sign: "npm:0.9.0"
semver: "npm:7.5.2"
zapier-platform-schema: "npm:15.4.1"
zapier-platform-schema: "npm:15.5.1"
dependenciesMeta:
"@types/node":
optional: true
checksum: 84ab2e0c65a436c9617f386219b82aac33d534f1af8104684f7037025cb916dd9e035ef18d82fe4c63039b9af95ea976b6d4561a123dea79dd20011252996ba8
checksum: af61f412fa8b4ce58a701d9bacd1600d2af59cb7eb80bc7206c7077cd5aa2cc69c4837e98b26872a3e43848d68ea2356521316ef74aadc35fb56f009352bc9b4
languageName: node
linkType: hard
"zapier-platform-schema@npm:15.4.1":
version: 15.4.1
resolution: "zapier-platform-schema@npm:15.4.1"
"zapier-platform-schema@npm:15.5.1":
version: 15.5.1
resolution: "zapier-platform-schema@npm:15.5.1"
dependencies:
jsonschema: "npm:1.2.2"
lodash: "npm:4.17.21"
checksum: a7b9de082aceeac345e4d03b740dd66f5131b7ab10a289e3db4e2548764c90495818fd6af366a6092c3c8d379533509482a6fcd88283381d424bfbda236e58b8
checksum: d08435d8221f7d6dbcb02a7a507bd63dbb9f2b9ee1d868da8d53e71e7fdc91426932957c2ff2afd8fc471749c17cf10a8d651e25f682af4c3fd0f11f5d76b5fd
languageName: node
linkType: hard