first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
IRequestOptions,
|
||||
IWebhookFunctions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
export async function jotformApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const credentials = await this.getCredentials('jotFormApi');
|
||||
let options: IRequestOptions = {
|
||||
headers: {
|
||||
APIKEY: credentials.apiKey,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
form: body,
|
||||
uri: uri || `https://${credentials.apiDomain || 'api.jotform.com'}${resource}`,
|
||||
json: true,
|
||||
};
|
||||
if (!Object.keys(body as IDataObject).length) {
|
||||
delete options.form;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.jotFormTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/jotform/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.jotformtrigger/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import type {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
MultiPartFormData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, jsonParse } from 'n8n-workflow';
|
||||
|
||||
import { jotformApiRequest } from './GenericFunctions';
|
||||
|
||||
interface IQuestionData {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class JotFormTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Jotform Trigger',
|
||||
name: 'jotFormTrigger',
|
||||
icon: { light: 'file:jotform.svg', dark: 'file:jotform.dark.svg' },
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Handle Jotform events via webhooks',
|
||||
defaults: {
|
||||
name: 'Jotform Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'jotFormApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Form Name or ID',
|
||||
name: 'form',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getForms',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolve Data',
|
||||
name: 'resolveData',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description:
|
||||
'By default does the webhook-data use internal keys instead of the names. If this option gets activated, it will resolve the keys automatically to the actual names.',
|
||||
},
|
||||
{
|
||||
displayName: 'Only Answers',
|
||||
name: 'onlyAnswers',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to return only the answers of the form and not any of the other data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the available forms to display them to user so that they can
|
||||
// select them easily
|
||||
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const qs: IDataObject = {
|
||||
limit: 1000,
|
||||
};
|
||||
const forms = await jotformApiRequest.call(this, 'GET', '/user/forms', {}, qs);
|
||||
|
||||
if (!Array.isArray(forms?.content)) return [];
|
||||
|
||||
for (const form of forms.content) {
|
||||
const formName = form.title;
|
||||
const formId = form.id;
|
||||
returnData.push({
|
||||
name: formName,
|
||||
value: formId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const formId = this.getNodeParameter('form') as string;
|
||||
const endpoint = `/form/${formId}/webhooks`;
|
||||
|
||||
try {
|
||||
const responseData = await jotformApiRequest.call(this, 'GET', endpoint);
|
||||
|
||||
const webhookUrls = Object.values(responseData.content as IDataObject);
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
if (!webhookUrls.includes(webhookUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhookIds = Object.keys(responseData.content as IDataObject);
|
||||
webhookData.webhookId = webhookIds[webhookUrls.indexOf(webhookUrl)];
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const formId = this.getNodeParameter('form') as string;
|
||||
const endpoint = `/form/${formId}/webhooks`;
|
||||
const body: IDataObject = {
|
||||
webhookURL: webhookUrl,
|
||||
};
|
||||
const { content } = await jotformApiRequest.call(this, 'POST', endpoint, body);
|
||||
webhookData.webhookId = Object.keys(content as IDataObject)[0];
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
let responseData;
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const formId = this.getNodeParameter('form') as string;
|
||||
const endpoint = `/form/${formId}/webhooks/${webhookData.webhookId}`;
|
||||
try {
|
||||
responseData = await jotformApiRequest.call(this, 'DELETE', endpoint);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
if (responseData.message !== 'success') {
|
||||
return false;
|
||||
}
|
||||
delete webhookData.webhookId;
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const req = this.getRequestObject() as MultiPartFormData.Request;
|
||||
const formId = this.getNodeParameter('form') as string;
|
||||
const resolveData = this.getNodeParameter('resolveData', false) as boolean;
|
||||
const onlyAnswers = this.getNodeParameter('onlyAnswers', false) as boolean;
|
||||
|
||||
const { data } = req.body;
|
||||
|
||||
const rawRequest = jsonParse<any>(data.rawRequest as string);
|
||||
data.rawRequest = rawRequest;
|
||||
|
||||
let returnData: IDataObject;
|
||||
if (!resolveData) {
|
||||
if (onlyAnswers) {
|
||||
returnData = data.rawRequest as unknown as IDataObject;
|
||||
} else {
|
||||
returnData = data;
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(returnData)],
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve the data by requesting the information via API
|
||||
const endpoint = `/form/${formId}/questions`;
|
||||
const responseData = await jotformApiRequest.call(this, 'GET', endpoint, {});
|
||||
|
||||
// Create a dictionary to resolve the keys
|
||||
const questionNames: IDataObject = {};
|
||||
for (const question of Object.values<IQuestionData>(responseData.content as IQuestionData[])) {
|
||||
questionNames[question.name] = question.text;
|
||||
}
|
||||
|
||||
// Resolve the keys
|
||||
let questionKey: string;
|
||||
const questionsData: IDataObject = {};
|
||||
for (const key of Object.keys(rawRequest as IDataObject)) {
|
||||
if (!key.includes('_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
questionKey = key.split('_').slice(1).join('_');
|
||||
if (questionNames[questionKey] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
questionsData[questionNames[questionKey] as string] = rawRequest[key];
|
||||
}
|
||||
|
||||
if (onlyAnswers) {
|
||||
returnData = questionsData as unknown as IDataObject;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
data.rawRequest = questionsData;
|
||||
returnData = data;
|
||||
}
|
||||
|
||||
return {
|
||||
workflowData: [this.helpers.returnJsonArray(returnData)],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6061_2)">
|
||||
<path d="M19.294 3.934C19.6043 3.62344 19.9727 3.37708 20.3783 3.20899C20.7838 3.0409 21.2185 2.95439 21.6575 2.95439C22.0965 2.95439 22.5312 3.0409 22.9367 3.20899C23.3423 3.37708 23.7107 3.62344 24.021 3.934C24.6478 4.56108 24.9999 5.41139 24.9999 6.298C24.9999 7.18461 24.6478 8.03492 24.021 8.662L12.204 20.482C11.8937 20.7924 11.5254 21.0386 11.1199 21.2066C10.7145 21.3746 10.2799 21.4611 9.841 21.4611C9.40212 21.4611 8.96754 21.3746 8.56209 21.2066C8.15664 21.0386 7.78826 20.7924 7.478 20.482C6.85153 19.855 6.49963 19.0049 6.49963 18.1185C6.49963 17.2321 6.85153 16.382 7.478 15.755L19.294 3.934Z" fill="#FF6100"/>
|
||||
<path d="M9.25 0.98C9.8824 0.38009 10.7241 0.0508301 11.5957 0.0623659C12.4673 0.0739016 13.2999 0.425321 13.9162 1.04176C14.5325 1.65819 14.8838 2.49092 14.8951 3.36253C14.9065 4.23413 14.577 5.07572 13.977 5.708L5.706 13.982C5.39565 14.2924 5.02719 14.5387 4.62166 14.7067C4.21612 14.8747 3.78146 14.9612 3.3425 14.9612C2.90354 14.9612 2.46888 14.8747 2.06334 14.7067C1.65781 14.5387 1.28935 14.2924 0.979 13.982C0.351918 13.3549 -0.000369849 12.5044 -0.000369849 11.6175C-0.000369849 10.7306 0.351918 9.88012 0.979 9.253L9.25 0.98Z" fill="#0099FF"/>
|
||||
<path d="M18.704 17.527C19.3371 16.931 20.1775 16.6051 21.0469 16.6183C21.9163 16.6316 22.7464 16.9829 23.3611 17.5979C23.9758 18.2128 24.3268 19.0431 24.3397 19.9125C24.3525 20.7819 24.0262 21.6221 23.43 22.255L18.704 26.984C18.3936 27.2944 18.025 27.5406 17.6194 27.7085C17.2138 27.8765 16.7791 27.9629 16.3401 27.9628C15.9012 27.9628 15.4665 27.8763 15.0609 27.7083C14.6554 27.5402 14.2869 27.2939 13.9765 26.9835C13.6661 26.6731 13.4199 26.3045 13.252 25.8989C13.084 25.4933 12.9976 25.0586 12.9977 24.6196C12.9977 24.1807 13.0842 23.746 13.2522 23.3404C13.4203 22.9349 13.6666 22.5664 13.977 22.256L18.704 17.527Z" fill="#FFB629"/>
|
||||
<path d="M1.671 28H6.338C7.083 28 7.456 27.1 6.929 26.573L1.426 21.07C0.9 20.542 0 20.915 0 21.659V26.329C0 27.251 0.748 28 1.671 28Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6061_2">
|
||||
<rect width="25" height="28" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 25 28"><path id="jotform-logomark-first" fill="#FF6100" d="M19.294 3.934a3.341 3.341 0 0 1 4.727 0 3.344 3.344 0 0 1 0 4.728l-11.817 11.82a3.341 3.341 0 0 1-4.726 0 3.344 3.344 0 0 1 0-4.727l11.816-11.82Z"></path><path id="jotform-logomark-second" fill="#0099FF" d="M9.25.98a3.344 3.344 0 0 1 4.727 4.728l-8.271 8.274a3.342 3.342 0 0 1-4.727 0 3.344 3.344 0 0 1 0-4.729L9.25.98Z"></path><path id="jotform-logomark-third" fill="#FFB629" d="M18.704 17.527a3.344 3.344 0 0 1 4.726 4.728l-4.726 4.729a3.342 3.342 0 1 1-4.727-4.728l4.727-4.729Z"></path><path id="jotform-logomark-fourth" fill="#0A1551" d="M1.671 28h4.667c.745 0 1.118-.9.591-1.427L1.426 21.07C.9 20.542 0 20.915 0 21.659v4.67C0 27.251.748 28 1.671 28Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 793 B |
Reference in New Issue
Block a user