diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json index 7c6464740b..539f015769 100644 --- a/packages/twenty-zapier/package.json +++ b/packages/twenty-zapier/package.json @@ -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", diff --git a/packages/twenty-zapier/src/creates/create_company.ts b/packages/twenty-zapier/src/creates/create_company.ts deleted file mode 100644 index 09c2781a69..0000000000 --- a/packages/twenty-zapier/src/creates/create_company.ts +++ /dev/null @@ -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, - }, -}; diff --git a/packages/twenty-zapier/src/creates/create_person.ts b/packages/twenty-zapier/src/creates/create_person.ts deleted file mode 100644 index 0e90a371d8..0000000000 --- a/packages/twenty-zapier/src/creates/create_person.ts +++ /dev/null @@ -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, - }, -}; diff --git a/packages/twenty-zapier/src/creates/create_record.ts b/packages/twenty-zapier/src/creates/create_record.ts new file mode 100644 index 0000000000..e68fb5b6e9 --- /dev/null +++ b/packages/twenty-zapier/src/creates/create_record.ts @@ -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 + }, +} diff --git a/packages/twenty-zapier/src/index.ts b/packages/twenty-zapier/src/index.ts index 49b875857a..698bef084e 100644 --- a/packages/twenty-zapier/src/index.ts +++ b/packages/twenty-zapier/src/index.ts @@ -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, }, }; diff --git a/packages/twenty-zapier/src/test/creates/create_person.test.ts b/packages/twenty-zapier/src/test/creates/create_person.test.ts deleted file mode 100644 index 1ede216b9b..0000000000 --- a/packages/twenty-zapier/src/test/creates/create_person.test.ts +++ /dev/null @@ -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(""); - }); -}); diff --git a/packages/twenty-zapier/src/test/creates/create_company.test.ts b/packages/twenty-zapier/src/test/creates/create_record.test.ts similarity index 58% rename from packages/twenty-zapier/src/test/creates/create_company.test.ts rename to packages/twenty-zapier/src/test/creates/create_record.test.ts index 8992a12e8b..dd1b0026f3 100644 --- a/packages/twenty-zapier/src/test/creates/create_company.test.ts +++ b/packages/twenty-zapier/src/test/creates/create_record.test.ts @@ -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'); + }) +}) diff --git a/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts b/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts new file mode 100644 index 0000000000..a8fab9e249 --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts @@ -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() + }) +}) diff --git a/packages/twenty-zapier/src/test/utils/capitalize.spec.ts b/packages/twenty-zapier/src/test/utils/capitalize.spec.ts new file mode 100644 index 0000000000..4ca2699901 --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/capitalize.spec.ts @@ -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') + }) +}) diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts new file mode 100644 index 0000000000..f7c2e34c6d --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts @@ -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" }, + ]) + }) +}) diff --git a/packages/twenty-zapier/src/test/utils/labelize.spec.ts b/packages/twenty-zapier/src/test/utils/labelize.spec.ts new file mode 100644 index 0000000000..002cc7dd33 --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/labelize.spec.ts @@ -0,0 +1,7 @@ +import { labelling } from "../../utils/labelling"; + +describe('labelling', ()=> { + test('should label properly', ()=> { + expect(labelling('createdAt')).toEqual('Created At') + }) +}) diff --git a/packages/twenty-zapier/src/triggers/find_objects.ts b/packages/twenty-zapier/src/triggers/find_objects.ts new file mode 100644 index 0000000000..62fff4aaf8 --- /dev/null +++ b/packages/twenty-zapier/src/triggers/find_objects.ts @@ -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, + }, +} diff --git a/packages/twenty-zapier/src/utils/capitalize.ts b/packages/twenty-zapier/src/utils/capitalize.ts new file mode 100644 index 0000000000..9cd7cc8932 --- /dev/null +++ b/packages/twenty-zapier/src/utils/capitalize.ts @@ -0,0 +1,3 @@ +export const capitalize = (word: string): string => { + return word.charAt(0).toUpperCase() + word.slice(1) +} diff --git a/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts new file mode 100644 index 0000000000..fa82329b3d --- /dev/null +++ b/packages/twenty-zapier/src/utils/computeInputFields.ts @@ -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 +} diff --git a/packages/twenty-zapier/src/utils/labelling.ts b/packages/twenty-zapier/src/utils/labelling.ts new file mode 100644 index 0000000000..a9eb709181 --- /dev/null +++ b/packages/twenty-zapier/src/utils/labelling.ts @@ -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(' '); +} diff --git a/packages/twenty-zapier/src/utils/requestDb.ts b/packages/twenty-zapier/src/utils/requestDb.ts index 5d7bf37fe8..d32c294f33 100644 --- a/packages/twenty-zapier/src/utils/requestDb.ts +++ b/packages/twenty-zapier/src/utils/requestDb.ts @@ -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`, diff --git a/packages/twenty-zapier/tsconfig.json b/packages/twenty-zapier/tsconfig.json index db63256e8b..1a3eb323e4 100644 --- a/packages/twenty-zapier/tsconfig.json +++ b/packages/twenty-zapier/tsconfig.json @@ -6,6 +6,8 @@ "lib": ["esnext"], "outDir": "./lib", "rootDir": "./src", - "strict": true + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true } } diff --git a/yarn.lock b/yarn.lock index 9580da0dd8..c1ebe74c84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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