2248 zapier integration implement typeorm eventsubscribers (#3122)

* Add new queue to twenty-server

* Add triggers to zapier

* Rename webhook operation

* Use find one or fail

* Use logger

* Fix typescript templating

* Add dedicated call webhook job

* Update logging

* Fix error handling
This commit is contained in:
martmull
2024-01-03 18:09:57 +01:00
committed by GitHub
parent 4ebaacc306
commit 65250839fb
36 changed files with 1040 additions and 209 deletions

View File

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

View File

@ -1,19 +1,19 @@
import { labelling } from "../utils/labelling";
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[]
}
properties?: { [field: string]: { type: string } };
items?: { [$ref: string]: string };
};
};
example: object;
required: string[];
};
export const computeInputFields = (infos: Infos): object[] => {
const result = []
const result = [];
for (const fieldName of Object.keys(infos.properties)) {
switch (infos.properties[fieldName].type) {
@ -23,17 +23,19 @@ export const computeInputFields = (infos: Infos): object[] => {
if (!infos.properties[fieldName].properties) {
break;
}
for (const subFieldName of Object.keys(infos.properties[fieldName].properties || {})) {
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
field.required = true;
}
result.push(field)
result.push(field);
}
break;
default:
@ -42,13 +44,13 @@ export const computeInputFields = (infos: Infos): object[] => {
label: labelling(fieldName),
type: infos.properties[fieldName].type,
required: false,
}
};
if (infos.required?.includes(fieldName)) {
field.required = true
field.required = true;
}
result.push(field)
result.push(field);
}
}
return result
}
return result;
};

View File

@ -1,25 +1,29 @@
const handleQueryParams = (inputData: { [x: string]: any }): string => {
const formattedInputData: {[x:string]: any} = {};
const formattedInputData: { [x: string]: any } = {};
Object.keys(inputData).forEach((key) => {
if(key.includes('__')) {
const [objectKey, nestedObjectKey] = key.split('__')
if (key.includes('__')) {
const [objectKey, nestedObjectKey] = key.split('__');
if (formattedInputData[objectKey]) {
formattedInputData[objectKey][nestedObjectKey] = inputData[key]
formattedInputData[objectKey][nestedObjectKey] = inputData[key];
} else {
formattedInputData[objectKey] = {[nestedObjectKey]: inputData[key]}
formattedInputData[objectKey] = { [nestedObjectKey]: inputData[key] };
}
} else {
formattedInputData[key]=inputData[key]
formattedInputData[key] = inputData[key];
}
})
});
let result = '';
Object.keys(formattedInputData).forEach((key) => {
let quote = '';
if (typeof formattedInputData[key]==='object') {
result=result.concat(`${key}: {${handleQueryParams(formattedInputData[key])}}, `)
if (typeof formattedInputData[key] === 'object') {
result = result.concat(
`${key}: {${handleQueryParams(formattedInputData[key])}}, `,
);
} else {
if (typeof formattedInputData[key] === 'string') quote = '"';
result = result.concat(`${key}: ${quote}${formattedInputData[key]}${quote}, `);
if (typeof formattedInputData[key] === 'string') quote = '"';
result = result.concat(
`${key}: ${quote}${formattedInputData[key]}${quote}, `,
);
}
});
if (result.length) result = result.slice(0, -2); // Remove the last ', '

View File

@ -1,9 +1,9 @@
import { capitalize } from "../utils/capitalize";
import { capitalize } from '../utils/capitalize';
export const labelling = (str: string): string => {
return str
.replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`)
.replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
.split(' ')
.map((word)=> capitalize(word))
.map((word) => capitalize(word))
.join(' ');
}
};

View File

@ -2,18 +2,17 @@ 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}`,
},
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)
}
return z.request(options).then((response) => response.json);
};
const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
const options = {
@ -37,7 +36,7 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
throw new z.errors.Error(
`query: ${query}, error: ${JSON.stringify(results.errors)}`,
'ApiError',
response.status
response.status,
);
}
response.throwForStatus();
@ -47,9 +46,32 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
throw new z.errors.Error(
`query: ${query}, error: ${err.message}`,
'Error',
err.status
err.status,
);
});
};
export const requestDbViaRestApi = (
z: ZObject,
bundle: Bundle,
objectNamePlural: string,
) => {
const options = {
url: `${process.env.SERVER_BASE_URL}/rest/${objectNamePlural}?limit:3`,
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.data[objectNamePlural])
.catch((err) => {
throw new z.errors.Error(`Error: ${err.message}`, 'Error', err.status);
});
};
export default requestDb;

View File

@ -0,0 +1,64 @@
import { Bundle, ZObject } from 'zapier-platform-core';
import requestDb, { requestDbViaRestApi } from '../utils/requestDb';
import handleQueryParams from '../utils/handleQueryParams';
export enum Operation {
create = 'create',
update = 'update',
delete = 'delete',
}
export const subscribe = async (
z: ZObject,
bundle: Bundle,
operation: Operation,
) => {
const data = {
targetUrl: bundle.targetUrl,
operation: `${operation}.${bundle.inputData.namePlural}`,
};
const result = await requestDb(
z,
bundle,
`mutation createWebhook {createWebhook(data:{${handleQueryParams(
data,
)}}) {id}}`,
);
return result.data.createWebhook;
};
export const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
const data = { id: bundle.subscribeData?.id };
const result = await requestDb(
z,
bundle,
`mutation deleteWebhook {deleteWebhook(${handleQueryParams(data)}) {id}}`,
);
return result.data.deleteWebhook;
};
export const perform = (z: ZObject, bundle: Bundle) => {
return [bundle.cleanedRequest];
};
export const listSample = async (
z: ZObject,
bundle: Bundle,
onlyIds = false,
) => {
const result: { [key: string]: string }[] = await requestDbViaRestApi(
z,
bundle,
bundle.inputData.namePlural,
);
if (onlyIds) {
return result.map((res) => {
return {
id: res.id,
};
});
}
return result;
};