n8n-nodes-formfex 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/credentials/FormfexApi.credentials.d.ts +21 -0
- package/dist/credentials/FormfexApi.credentials.js +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/nodes/Formfex/Formfex.node.d.ts +5 -0
- package/dist/nodes/Formfex/Formfex.node.js +268 -0
- package/dist/nodes/Formfex/FormfexTrigger.node.d.ts +5 -0
- package/dist/nodes/Formfex/FormfexTrigger.node.js +63 -0
- package/dist/nodes/Formfex/descriptions/ai.description.d.ts +3 -0
- package/dist/nodes/Formfex/descriptions/ai.description.js +108 -0
- package/dist/nodes/Formfex/descriptions/form.description.d.ts +3 -0
- package/dist/nodes/Formfex/descriptions/form.description.js +121 -0
- package/dist/nodes/Formfex/descriptions/response.description.d.ts +3 -0
- package/dist/nodes/Formfex/descriptions/response.description.js +80 -0
- package/dist/nodes/Formfex/helpers.d.ts +12 -0
- package/dist/nodes/Formfex/helpers.js +64 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Formfex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# n8n-nodes-formfex
|
|
2
|
+
|
|
3
|
+
[n8n](https://n8n.io/) community node for [Formfex](https://formfex.com) — AI-powered form builder.
|
|
4
|
+
|
|
5
|
+
Create forms, collect responses, trigger automations, and generate forms with AI directly from your n8n workflows.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### Community Nodes (Recommended)
|
|
13
|
+
|
|
14
|
+
1. Go to **Settings > Community Nodes** in your n8n instance
|
|
15
|
+
2. Click **Install a community node**
|
|
16
|
+
3. Enter `n8n-nodes-formfex`
|
|
17
|
+
4. Click **Install**
|
|
18
|
+
|
|
19
|
+
### Manual Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd ~/.n8n/nodes
|
|
23
|
+
npm install n8n-nodes-formfex
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Restart n8n after installation.
|
|
27
|
+
|
|
28
|
+
## Credentials
|
|
29
|
+
|
|
30
|
+
You need a Formfex API key to use this node.
|
|
31
|
+
|
|
32
|
+
1. Log in to [Formfex](https://formfex.com)
|
|
33
|
+
2. Go to **Account > Integrations > API Keys**
|
|
34
|
+
3. Click **Create** and select the permissions you need
|
|
35
|
+
4. Copy the key (format: `fxk_live_<keyId>.<secret>`)
|
|
36
|
+
5. In n8n, add a new **Formfex API** credential and paste the key
|
|
37
|
+
|
|
38
|
+
> API keys require a **Starter plan** or higher.
|
|
39
|
+
|
|
40
|
+
## Nodes
|
|
41
|
+
|
|
42
|
+
### Formfex
|
|
43
|
+
|
|
44
|
+
Regular node with three resources:
|
|
45
|
+
|
|
46
|
+
#### Form Operations
|
|
47
|
+
|
|
48
|
+
| Operation | Description |
|
|
49
|
+
|-----------|-------------|
|
|
50
|
+
| **Create** | Create a new form |
|
|
51
|
+
| **Get** | Get a form by ID |
|
|
52
|
+
| **Get Many** | List forms (with pagination and search) |
|
|
53
|
+
| **Update** | Update a form |
|
|
54
|
+
| **Delete** | Delete a form |
|
|
55
|
+
| **Publish** | Publish a form |
|
|
56
|
+
| **Unpublish** | Unpublish a form |
|
|
57
|
+
|
|
58
|
+
#### Response Operations
|
|
59
|
+
|
|
60
|
+
| Operation | Description |
|
|
61
|
+
|-----------|-------------|
|
|
62
|
+
| **Get** | Get a single response by ID |
|
|
63
|
+
| **Get Many** | List responses (with date filters and pagination) |
|
|
64
|
+
|
|
65
|
+
#### AI Operations
|
|
66
|
+
|
|
67
|
+
| Operation | Description |
|
|
68
|
+
|-----------|-------------|
|
|
69
|
+
| **Generate Form** | Generate a form from a text prompt using AI (polls until complete) |
|
|
70
|
+
| **Get Job Status** | Check the status of an AI generation job |
|
|
71
|
+
| **Smart Analytics** | Dispatch an async smart analytics report (results sent to callback URL) |
|
|
72
|
+
| **Analytics Chat** | Ask natural language questions about form analytics data |
|
|
73
|
+
|
|
74
|
+
### Formfex Trigger
|
|
75
|
+
|
|
76
|
+
Polling trigger that watches for new form responses.
|
|
77
|
+
|
|
78
|
+
| Parameter | Description |
|
|
79
|
+
|-----------|-------------|
|
|
80
|
+
| **Form ID** | The UUID of the form to watch |
|
|
81
|
+
|
|
82
|
+
The trigger polls periodically and returns any new responses since the last check. Configure the polling interval in your n8n workflow settings.
|
|
83
|
+
|
|
84
|
+
## AI Agent Tool Mode
|
|
85
|
+
|
|
86
|
+
The Formfex node has `usableAsTool: true`, which means it can be used as a tool inside **n8n AI Agent** nodes. This allows AI agents to autonomously create forms, fetch responses, and generate analytics.
|
|
87
|
+
|
|
88
|
+
## Supported Languages
|
|
89
|
+
|
|
90
|
+
Formfex supports 6 languages for form generation and content:
|
|
91
|
+
|
|
92
|
+
- English (`en`)
|
|
93
|
+
- Turkish (`tr`)
|
|
94
|
+
- Spanish (`es`)
|
|
95
|
+
- Italian (`it`)
|
|
96
|
+
- German (`de`)
|
|
97
|
+
- Dutch (`nl`)
|
|
98
|
+
|
|
99
|
+
## API Scopes
|
|
100
|
+
|
|
101
|
+
When creating an API key, select the scopes your workflow needs:
|
|
102
|
+
|
|
103
|
+
| Scope | Description |
|
|
104
|
+
|-------|-------------|
|
|
105
|
+
| `FORMS_READ` | List and view forms |
|
|
106
|
+
| `FORMS_WRITE` | Create, edit, delete forms |
|
|
107
|
+
| `RESPONSES_READ` | List and view form submissions |
|
|
108
|
+
| `WEBHOOKS_READ` | List and view webhooks |
|
|
109
|
+
| `WEBHOOKS_WRITE` | Create, edit, delete webhooks |
|
|
110
|
+
| `AI_GENERATE` | Generate forms with AI |
|
|
111
|
+
| `ANALYTICS_READ` | Access smart analytics and analytics chat |
|
|
112
|
+
|
|
113
|
+
## Resources
|
|
114
|
+
|
|
115
|
+
- [Formfex Website](https://formfex.com)
|
|
116
|
+
- [Formfex API Documentation](https://docs.formfex.com/api)
|
|
117
|
+
- [n8n Community Nodes Documentation](https://docs.n8n.io/integrations/community-nodes/)
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class FormfexApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
properties: INodeProperties[];
|
|
7
|
+
authenticate: {
|
|
8
|
+
type: "generic";
|
|
9
|
+
properties: {
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
test: {
|
|
16
|
+
request: {
|
|
17
|
+
baseURL: string;
|
|
18
|
+
url: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormfexApi = void 0;
|
|
4
|
+
const FORMFEX_API_BASE_URL = 'https://api.formfex.com';
|
|
5
|
+
class FormfexApi {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.name = 'formfexApi';
|
|
8
|
+
this.displayName = 'Formfex API';
|
|
9
|
+
this.documentationUrl = 'https://docs.formfex.com/api';
|
|
10
|
+
this.properties = [
|
|
11
|
+
{
|
|
12
|
+
displayName: 'API Key',
|
|
13
|
+
name: 'apiKey',
|
|
14
|
+
type: 'string',
|
|
15
|
+
typeOptions: { password: true },
|
|
16
|
+
default: '',
|
|
17
|
+
required: true,
|
|
18
|
+
placeholder: 'fxk_live_...',
|
|
19
|
+
description: 'Your Formfex API key (format: fxk_live_{keyId}.{secret}). Get it from Account > Integrations > API Keys.',
|
|
20
|
+
},
|
|
21
|
+
// No baseUrl field — hardcoded to prevent SSRF on shared n8n instances
|
|
22
|
+
];
|
|
23
|
+
this.authenticate = {
|
|
24
|
+
type: 'generic',
|
|
25
|
+
properties: {
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: '=Bearer {{$credentials.apiKey}}',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
this.test = {
|
|
32
|
+
request: {
|
|
33
|
+
baseURL: FORMFEX_API_BASE_URL,
|
|
34
|
+
url: '/api/v1/public/me',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.FormfexApi = FormfexApi;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormfexTrigger = exports.Formfex = exports.FormfexApi = void 0;
|
|
4
|
+
var FormfexApi_credentials_1 = require("./credentials/FormfexApi.credentials");
|
|
5
|
+
Object.defineProperty(exports, "FormfexApi", { enumerable: true, get: function () { return FormfexApi_credentials_1.FormfexApi; } });
|
|
6
|
+
var Formfex_node_1 = require("./nodes/Formfex/Formfex.node");
|
|
7
|
+
Object.defineProperty(exports, "Formfex", { enumerable: true, get: function () { return Formfex_node_1.Formfex; } });
|
|
8
|
+
var FormfexTrigger_node_1 = require("./nodes/Formfex/FormfexTrigger.node");
|
|
9
|
+
Object.defineProperty(exports, "FormfexTrigger", { enumerable: true, get: function () { return FormfexTrigger_node_1.FormfexTrigger; } });
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Formfex = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const form_description_1 = require("./descriptions/form.description");
|
|
6
|
+
const response_description_1 = require("./descriptions/response.description");
|
|
7
|
+
const ai_description_1 = require("./descriptions/ai.description");
|
|
8
|
+
const helpers_1 = require("./helpers");
|
|
9
|
+
class Formfex {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.description = {
|
|
12
|
+
displayName: 'Formfex',
|
|
13
|
+
name: 'formfex',
|
|
14
|
+
icon: 'file:Formfex.svg',
|
|
15
|
+
group: ['transform'],
|
|
16
|
+
version: 1,
|
|
17
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
18
|
+
description: 'Interact with Formfex forms, responses, and AI features',
|
|
19
|
+
defaults: { name: 'Formfex' },
|
|
20
|
+
inputs: ['main'],
|
|
21
|
+
outputs: ['main'],
|
|
22
|
+
usableAsTool: true,
|
|
23
|
+
credentials: [{ name: 'formfexApi', required: true }],
|
|
24
|
+
requestDefaults: {
|
|
25
|
+
baseURL: 'https://api.formfex.com',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
},
|
|
28
|
+
properties: [
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Resource',
|
|
31
|
+
name: 'resource',
|
|
32
|
+
type: 'options',
|
|
33
|
+
noDataExpression: true,
|
|
34
|
+
options: [
|
|
35
|
+
{ name: 'AI', value: 'ai' },
|
|
36
|
+
{ name: 'Form', value: 'form' },
|
|
37
|
+
{ name: 'Response', value: 'response' },
|
|
38
|
+
],
|
|
39
|
+
default: 'form',
|
|
40
|
+
},
|
|
41
|
+
...form_description_1.formOperations,
|
|
42
|
+
...form_description_1.formFields,
|
|
43
|
+
...response_description_1.responseOperations,
|
|
44
|
+
...response_description_1.responseFields,
|
|
45
|
+
...ai_description_1.aiOperations,
|
|
46
|
+
...ai_description_1.aiFields,
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async execute() {
|
|
51
|
+
const items = this.getInputData();
|
|
52
|
+
const returnData = [];
|
|
53
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
54
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
55
|
+
for (let i = 0; i < items.length; i++) {
|
|
56
|
+
try {
|
|
57
|
+
let responseData;
|
|
58
|
+
if (resource === 'form') {
|
|
59
|
+
responseData = await executeFormOperation.call(this, operation, i);
|
|
60
|
+
}
|
|
61
|
+
else if (resource === 'response') {
|
|
62
|
+
responseData = await executeResponseOperation.call(this, operation, i);
|
|
63
|
+
}
|
|
64
|
+
else if (resource === 'ai') {
|
|
65
|
+
responseData = await executeAiOperation.call(this, operation, i);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(responseData)) {
|
|
68
|
+
returnData.push(...responseData.map((item) => ({ json: item })));
|
|
69
|
+
}
|
|
70
|
+
else if (responseData) {
|
|
71
|
+
returnData.push({ json: responseData });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (this.continueOnFail()) {
|
|
76
|
+
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [returnData];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.Formfex = Formfex;
|
|
86
|
+
async function executeFormOperation(operation, i) {
|
|
87
|
+
if (operation === 'create') {
|
|
88
|
+
const title = this.getNodeParameter('title', i);
|
|
89
|
+
const additionalFields = this.getNodeParameter('additionalFields', i, {});
|
|
90
|
+
const body = { title };
|
|
91
|
+
if (additionalFields.description)
|
|
92
|
+
body.description = additionalFields.description;
|
|
93
|
+
if (additionalFields.language)
|
|
94
|
+
body.language = additionalFields.language;
|
|
95
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'POST', '/forms', body);
|
|
96
|
+
return result.data;
|
|
97
|
+
}
|
|
98
|
+
if (operation === 'get') {
|
|
99
|
+
const formId = this.getNodeParameter('formId', i);
|
|
100
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
101
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/forms/${(0, helpers_1.safePath)(formId)}`);
|
|
102
|
+
return result.data;
|
|
103
|
+
}
|
|
104
|
+
if (operation === 'getMany') {
|
|
105
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
106
|
+
const filters = this.getNodeParameter('filters', i, {});
|
|
107
|
+
const limit = returnAll ? 100 : this.getNodeParameter('limit', i);
|
|
108
|
+
const query = { limit, page: 1 };
|
|
109
|
+
if (filters.search)
|
|
110
|
+
query.search = filters.search;
|
|
111
|
+
if (filters.since)
|
|
112
|
+
query.since = filters.since;
|
|
113
|
+
if (!returnAll) {
|
|
114
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', '/forms', undefined, query);
|
|
115
|
+
return result.data.forms ?? result.data;
|
|
116
|
+
}
|
|
117
|
+
// Paginate
|
|
118
|
+
const allItems = [];
|
|
119
|
+
let page = 1;
|
|
120
|
+
let hasMore = true;
|
|
121
|
+
while (hasMore && page <= helpers_1.MAX_PAGES) {
|
|
122
|
+
query.page = page;
|
|
123
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', '/forms', undefined, query);
|
|
124
|
+
const items = result.data.forms ?? result.data ?? [];
|
|
125
|
+
allItems.push(...items);
|
|
126
|
+
hasMore = result.data.hasMore ?? items.length === limit;
|
|
127
|
+
page++;
|
|
128
|
+
}
|
|
129
|
+
if (page > helpers_1.MAX_PAGES) {
|
|
130
|
+
allItems.push({ _warning: `Return All truncated at ${helpers_1.MAX_PAGES * limit} records. Use filters to narrow the dataset.` });
|
|
131
|
+
}
|
|
132
|
+
return allItems;
|
|
133
|
+
}
|
|
134
|
+
if (operation === 'update') {
|
|
135
|
+
const formId = this.getNodeParameter('formId', i);
|
|
136
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
137
|
+
const updateFields = this.getNodeParameter('updateFields', i, {});
|
|
138
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'PATCH', `/forms/${(0, helpers_1.safePath)(formId)}`, updateFields);
|
|
139
|
+
return result.data;
|
|
140
|
+
}
|
|
141
|
+
if (operation === 'delete') {
|
|
142
|
+
const formId = this.getNodeParameter('formId', i);
|
|
143
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
144
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'DELETE', `/forms/${(0, helpers_1.safePath)(formId)}`);
|
|
145
|
+
return result.data ?? { success: true };
|
|
146
|
+
}
|
|
147
|
+
if (operation === 'publish') {
|
|
148
|
+
const formId = this.getNodeParameter('formId', i);
|
|
149
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
150
|
+
const publishFields = this.getNodeParameter('publishFields', i, {});
|
|
151
|
+
const body = {};
|
|
152
|
+
if (publishFields.visibility)
|
|
153
|
+
body.visibility = publishFields.visibility;
|
|
154
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'POST', `/forms/${(0, helpers_1.safePath)(formId)}/publish`, body);
|
|
155
|
+
return result.data;
|
|
156
|
+
}
|
|
157
|
+
if (operation === 'unpublish') {
|
|
158
|
+
const formId = this.getNodeParameter('formId', i);
|
|
159
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
160
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'POST', `/forms/${(0, helpers_1.safePath)(formId)}/unpublish`);
|
|
161
|
+
return result.data;
|
|
162
|
+
}
|
|
163
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Unknown operation: ${operation}` });
|
|
164
|
+
}
|
|
165
|
+
async function executeResponseOperation(operation, i) {
|
|
166
|
+
const formId = this.getNodeParameter('formId', i);
|
|
167
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
168
|
+
if (operation === 'get') {
|
|
169
|
+
const responseId = this.getNodeParameter('responseId', i);
|
|
170
|
+
(0, helpers_1.validateUuid)(this, responseId, 'Response ID');
|
|
171
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/forms/${(0, helpers_1.safePath)(formId)}/responses/${(0, helpers_1.safePath)(responseId)}`);
|
|
172
|
+
return result.data;
|
|
173
|
+
}
|
|
174
|
+
if (operation === 'getMany') {
|
|
175
|
+
const returnAll = this.getNodeParameter('returnAll', i);
|
|
176
|
+
const filters = this.getNodeParameter('filters', i, {});
|
|
177
|
+
const limit = returnAll ? 100 : this.getNodeParameter('limit', i);
|
|
178
|
+
const query = { limit, page: 1 };
|
|
179
|
+
if (filters.startDate)
|
|
180
|
+
query.startDate = filters.startDate;
|
|
181
|
+
if (filters.endDate)
|
|
182
|
+
query.endDate = filters.endDate;
|
|
183
|
+
if (filters.since)
|
|
184
|
+
query.since = filters.since;
|
|
185
|
+
if (filters.order)
|
|
186
|
+
query.order = filters.order;
|
|
187
|
+
if (!returnAll) {
|
|
188
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/forms/${(0, helpers_1.safePath)(formId)}/responses`, undefined, query);
|
|
189
|
+
return result.data.items ?? result.data;
|
|
190
|
+
}
|
|
191
|
+
// Paginate
|
|
192
|
+
const allItems = [];
|
|
193
|
+
let page = 1;
|
|
194
|
+
let hasMore = true;
|
|
195
|
+
while (hasMore && page <= helpers_1.MAX_PAGES) {
|
|
196
|
+
query.page = page;
|
|
197
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/forms/${(0, helpers_1.safePath)(formId)}/responses`, undefined, query);
|
|
198
|
+
const items = result.data.items ?? result.data ?? [];
|
|
199
|
+
allItems.push(...items);
|
|
200
|
+
hasMore = result.data.hasMore ?? items.length === limit;
|
|
201
|
+
page++;
|
|
202
|
+
}
|
|
203
|
+
if (page > helpers_1.MAX_PAGES) {
|
|
204
|
+
allItems.push({ _warning: `Return All truncated at ${helpers_1.MAX_PAGES * limit} records. Use date filters to narrow the dataset.` });
|
|
205
|
+
}
|
|
206
|
+
return allItems;
|
|
207
|
+
}
|
|
208
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Unknown operation: ${operation}` });
|
|
209
|
+
}
|
|
210
|
+
async function executeAiOperation(operation, i) {
|
|
211
|
+
if (operation === 'generateForm') {
|
|
212
|
+
const prompt = this.getNodeParameter('prompt', i);
|
|
213
|
+
const language = this.getNodeParameter('language', i, 'en');
|
|
214
|
+
// Dispatch the job
|
|
215
|
+
const dispatchResult = await helpers_1.formfexApiRequest.call(this, 'POST', '/ai/generate-form', { prompt, language });
|
|
216
|
+
const jobId = dispatchResult.data.jobId;
|
|
217
|
+
// Poll until done (max 20 attempts, 3s interval = 60s max)
|
|
218
|
+
const MAX_POLL_ATTEMPTS = 20;
|
|
219
|
+
const POLL_INTERVAL_MS = 3000;
|
|
220
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
221
|
+
await sleep(POLL_INTERVAL_MS);
|
|
222
|
+
const jobResult = await helpers_1.formfexApiRequest.call(this, 'GET', `/ai/jobs/${(0, helpers_1.safePath)(jobId)}`);
|
|
223
|
+
const job = jobResult.data;
|
|
224
|
+
if (job.status === 'DONE') {
|
|
225
|
+
return job;
|
|
226
|
+
}
|
|
227
|
+
if (job.status === 'FAILED') {
|
|
228
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
229
|
+
message: 'AI form generation failed',
|
|
230
|
+
description: job.error ?? 'Unknown error',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// PENDING or PROCESSING — keep polling
|
|
234
|
+
}
|
|
235
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
236
|
+
message: 'AI form generation timed out',
|
|
237
|
+
description: `Job ${jobId} did not complete within ${MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS / 1000} seconds`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (operation === 'getJobStatus') {
|
|
241
|
+
const jobId = this.getNodeParameter('jobId', i);
|
|
242
|
+
(0, helpers_1.validateUuid)(this, jobId, 'Job ID');
|
|
243
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/ai/jobs/${(0, helpers_1.safePath)(jobId)}`);
|
|
244
|
+
return result.data;
|
|
245
|
+
}
|
|
246
|
+
if (operation === 'smartAnalytics') {
|
|
247
|
+
const formId = this.getNodeParameter('formId', i);
|
|
248
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
249
|
+
const callbackUrl = this.getNodeParameter('callbackUrl', i);
|
|
250
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'POST', `/forms/${(0, helpers_1.safePath)(formId)}/smart-analytics`, { callbackUrl });
|
|
251
|
+
return result.data;
|
|
252
|
+
}
|
|
253
|
+
if (operation === 'analyticsChat') {
|
|
254
|
+
const formId = this.getNodeParameter('formId', i);
|
|
255
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
256
|
+
const message = this.getNodeParameter('message', i);
|
|
257
|
+
const sessionId = this.getNodeParameter('sessionId', i, '');
|
|
258
|
+
const body = { message };
|
|
259
|
+
if (sessionId)
|
|
260
|
+
body.sessionId = sessionId;
|
|
261
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'POST', `/forms/${(0, helpers_1.safePath)(formId)}/analytics-chat`, body);
|
|
262
|
+
return result.data;
|
|
263
|
+
}
|
|
264
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: `Unknown operation: ${operation}` });
|
|
265
|
+
}
|
|
266
|
+
function sleep(ms) {
|
|
267
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
268
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IPollFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class FormfexTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FormfexTrigger = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
class FormfexTrigger {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Formfex Trigger',
|
|
9
|
+
name: 'formfexTrigger',
|
|
10
|
+
icon: 'file:Formfex.svg',
|
|
11
|
+
group: ['trigger'],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '=New Response on Form',
|
|
14
|
+
description: 'Triggers when a new form response is submitted',
|
|
15
|
+
defaults: { name: 'Formfex Trigger' },
|
|
16
|
+
inputs: [],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
credentials: [{ name: 'formfexApi', required: true }],
|
|
19
|
+
polling: true,
|
|
20
|
+
properties: [
|
|
21
|
+
{
|
|
22
|
+
displayName: 'Form ID',
|
|
23
|
+
name: 'formId',
|
|
24
|
+
type: 'string',
|
|
25
|
+
required: true,
|
|
26
|
+
default: '',
|
|
27
|
+
description: 'The UUID of the form to watch for new responses',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async poll() {
|
|
33
|
+
const formId = this.getNodeParameter('formId');
|
|
34
|
+
(0, helpers_1.validateUuid)(this, formId, 'Form ID');
|
|
35
|
+
const staticData = this.getWorkflowStaticData('node');
|
|
36
|
+
const lastTimeChecked = staticData.lastTimeChecked ?? new Date().toISOString();
|
|
37
|
+
const query = {
|
|
38
|
+
since: lastTimeChecked,
|
|
39
|
+
order: 'newest_first',
|
|
40
|
+
limit: 100,
|
|
41
|
+
page: 1,
|
|
42
|
+
};
|
|
43
|
+
const allItems = [];
|
|
44
|
+
let hasMore = true;
|
|
45
|
+
let page = 1;
|
|
46
|
+
const MAX_POLL_PAGES = 10; // Safety cap for polling
|
|
47
|
+
while (hasMore && page <= MAX_POLL_PAGES) {
|
|
48
|
+
query.page = page;
|
|
49
|
+
const result = await helpers_1.formfexApiRequest.call(this, 'GET', `/forms/${(0, helpers_1.safePath)(formId)}/responses`, undefined, query);
|
|
50
|
+
const items = result.data?.items ?? [];
|
|
51
|
+
allItems.push(...items);
|
|
52
|
+
hasMore = result.data?.hasMore ?? items.length === 100;
|
|
53
|
+
page++;
|
|
54
|
+
}
|
|
55
|
+
// Update the checkpoint
|
|
56
|
+
staticData.lastTimeChecked = new Date().toISOString();
|
|
57
|
+
if (allItems.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return [allItems.map((item) => ({ json: item }))];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.FormfexTrigger = FormfexTrigger;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aiFields = exports.aiOperations = void 0;
|
|
4
|
+
exports.aiOperations = [
|
|
5
|
+
{
|
|
6
|
+
displayName: 'Operation',
|
|
7
|
+
name: 'operation',
|
|
8
|
+
type: 'options',
|
|
9
|
+
noDataExpression: true,
|
|
10
|
+
displayOptions: { show: { resource: ['ai'] } },
|
|
11
|
+
options: [
|
|
12
|
+
{ name: 'Analytics Chat', value: 'analyticsChat', action: 'Chat about analytics', description: 'Ask questions about form analytics data' },
|
|
13
|
+
{ name: 'Generate Form', value: 'generateForm', action: 'Generate form with AI', description: 'Generate a form from a text prompt using AI' },
|
|
14
|
+
{ name: 'Get Job Status', value: 'getJobStatus', action: 'Get AI job status', description: 'Check the status of an AI generation job' },
|
|
15
|
+
{ name: 'Smart Analytics', value: 'smartAnalytics', action: 'Dispatch smart analytics', description: 'Generate a smart analytics report' },
|
|
16
|
+
],
|
|
17
|
+
default: 'generateForm',
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
exports.aiFields = [
|
|
21
|
+
// ── Generate Form ──
|
|
22
|
+
{
|
|
23
|
+
displayName: 'Prompt',
|
|
24
|
+
name: 'prompt',
|
|
25
|
+
type: 'string',
|
|
26
|
+
typeOptions: { rows: 4 },
|
|
27
|
+
required: true,
|
|
28
|
+
default: '',
|
|
29
|
+
placeholder: 'A customer satisfaction survey with rating scales and open-ended feedback',
|
|
30
|
+
description: 'Describe the form you want AI to generate (max 400 characters)',
|
|
31
|
+
displayOptions: { show: { resource: ['ai'], operation: ['generateForm'] } },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
displayName: 'Language',
|
|
35
|
+
name: 'language',
|
|
36
|
+
type: 'options',
|
|
37
|
+
options: [
|
|
38
|
+
{ name: 'English', value: 'en' },
|
|
39
|
+
{ name: 'Turkish', value: 'tr' },
|
|
40
|
+
{ name: 'Spanish', value: 'es' },
|
|
41
|
+
{ name: 'Italian', value: 'it' },
|
|
42
|
+
{ name: 'German', value: 'de' },
|
|
43
|
+
{ name: 'Dutch', value: 'nl' },
|
|
44
|
+
],
|
|
45
|
+
default: 'en',
|
|
46
|
+
description: 'Language for the generated form',
|
|
47
|
+
displayOptions: { show: { resource: ['ai'], operation: ['generateForm'] } },
|
|
48
|
+
},
|
|
49
|
+
// ── Get Job Status ──
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Job ID',
|
|
52
|
+
name: 'jobId',
|
|
53
|
+
type: 'string',
|
|
54
|
+
required: true,
|
|
55
|
+
default: '',
|
|
56
|
+
description: 'The UUID of the AI job to check',
|
|
57
|
+
displayOptions: { show: { resource: ['ai'], operation: ['getJobStatus'] } },
|
|
58
|
+
},
|
|
59
|
+
// ── Smart Analytics ──
|
|
60
|
+
{
|
|
61
|
+
displayName: 'Form ID',
|
|
62
|
+
name: 'formId',
|
|
63
|
+
type: 'string',
|
|
64
|
+
required: true,
|
|
65
|
+
default: '',
|
|
66
|
+
description: 'The UUID of the form to analyze',
|
|
67
|
+
displayOptions: { show: { resource: ['ai'], operation: ['smartAnalytics'] } },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
displayName: 'Callback URL',
|
|
71
|
+
name: 'callbackUrl',
|
|
72
|
+
type: 'string',
|
|
73
|
+
required: true,
|
|
74
|
+
default: '',
|
|
75
|
+
placeholder: 'https://your-server.com/webhook/formfex-analytics',
|
|
76
|
+
description: 'HTTPS URL where the analytics results will be sent when ready',
|
|
77
|
+
displayOptions: { show: { resource: ['ai'], operation: ['smartAnalytics'] } },
|
|
78
|
+
},
|
|
79
|
+
// ── Analytics Chat ──
|
|
80
|
+
{
|
|
81
|
+
displayName: 'Form ID',
|
|
82
|
+
name: 'formId',
|
|
83
|
+
type: 'string',
|
|
84
|
+
required: true,
|
|
85
|
+
default: '',
|
|
86
|
+
description: 'The UUID of the form to chat about',
|
|
87
|
+
displayOptions: { show: { resource: ['ai'], operation: ['analyticsChat'] } },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
displayName: 'Message',
|
|
91
|
+
name: 'message',
|
|
92
|
+
type: 'string',
|
|
93
|
+
typeOptions: { rows: 2 },
|
|
94
|
+
required: true,
|
|
95
|
+
default: '',
|
|
96
|
+
placeholder: 'What are the most common responses?',
|
|
97
|
+
description: 'Your question about the form analytics data',
|
|
98
|
+
displayOptions: { show: { resource: ['ai'], operation: ['analyticsChat'] } },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
displayName: 'Session ID',
|
|
102
|
+
name: 'sessionId',
|
|
103
|
+
type: 'string',
|
|
104
|
+
default: '',
|
|
105
|
+
description: 'Session ID for multi-turn conversation. Leave empty to start a new session.',
|
|
106
|
+
displayOptions: { show: { resource: ['ai'], operation: ['analyticsChat'] } },
|
|
107
|
+
},
|
|
108
|
+
];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formFields = exports.formOperations = void 0;
|
|
4
|
+
exports.formOperations = [
|
|
5
|
+
{
|
|
6
|
+
displayName: 'Operation',
|
|
7
|
+
name: 'operation',
|
|
8
|
+
type: 'options',
|
|
9
|
+
noDataExpression: true,
|
|
10
|
+
displayOptions: { show: { resource: ['form'] } },
|
|
11
|
+
options: [
|
|
12
|
+
{ name: 'Create', value: 'create', action: 'Create a form', description: 'Create a new form' },
|
|
13
|
+
{ name: 'Delete', value: 'delete', action: 'Delete a form', description: 'Delete a form' },
|
|
14
|
+
{ name: 'Get', value: 'get', action: 'Get a form', description: 'Get a form by ID' },
|
|
15
|
+
{ name: 'Get Many', value: 'getMany', action: 'Get many forms', description: 'List forms (paginated)' },
|
|
16
|
+
{ name: 'Publish', value: 'publish', action: 'Publish a form', description: 'Publish a form' },
|
|
17
|
+
{ name: 'Unpublish', value: 'unpublish', action: 'Unpublish a form', description: 'Unpublish a form' },
|
|
18
|
+
{ name: 'Update', value: 'update', action: 'Update a form', description: 'Update a form' },
|
|
19
|
+
],
|
|
20
|
+
default: 'getMany',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
exports.formFields = [
|
|
24
|
+
// ── Form ID (shared) ──
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Form ID',
|
|
27
|
+
name: 'formId',
|
|
28
|
+
type: 'string',
|
|
29
|
+
required: true,
|
|
30
|
+
default: '',
|
|
31
|
+
description: 'The UUID of the form',
|
|
32
|
+
displayOptions: {
|
|
33
|
+
show: { resource: ['form'], operation: ['get', 'update', 'delete', 'publish', 'unpublish'] },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
// ── Create Fields ──
|
|
37
|
+
{
|
|
38
|
+
displayName: 'Title',
|
|
39
|
+
name: 'title',
|
|
40
|
+
type: 'string',
|
|
41
|
+
required: true,
|
|
42
|
+
default: '',
|
|
43
|
+
description: 'Form title',
|
|
44
|
+
displayOptions: { show: { resource: ['form'], operation: ['create'] } },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
displayName: 'Additional Fields',
|
|
48
|
+
name: 'additionalFields',
|
|
49
|
+
type: 'collection',
|
|
50
|
+
placeholder: 'Add Field',
|
|
51
|
+
default: {},
|
|
52
|
+
displayOptions: { show: { resource: ['form'], operation: ['create'] } },
|
|
53
|
+
options: [
|
|
54
|
+
{ displayName: 'Description', name: 'description', type: 'string', default: '' },
|
|
55
|
+
{ displayName: 'Language', name: 'language', type: 'string', default: 'en', description: 'Form language code (en, tr, es, it, de, nl)' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
// ── Update Fields ──
|
|
59
|
+
{
|
|
60
|
+
displayName: 'Update Fields',
|
|
61
|
+
name: 'updateFields',
|
|
62
|
+
type: 'collection',
|
|
63
|
+
placeholder: 'Add Field',
|
|
64
|
+
default: {},
|
|
65
|
+
displayOptions: { show: { resource: ['form'], operation: ['update'] } },
|
|
66
|
+
options: [
|
|
67
|
+
{ displayName: 'Title', name: 'title', type: 'string', default: '' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
// ── Publish Fields ──
|
|
71
|
+
{
|
|
72
|
+
displayName: 'Additional Fields',
|
|
73
|
+
name: 'publishFields',
|
|
74
|
+
type: 'collection',
|
|
75
|
+
placeholder: 'Add Field',
|
|
76
|
+
default: {},
|
|
77
|
+
displayOptions: { show: { resource: ['form'], operation: ['publish'] } },
|
|
78
|
+
options: [
|
|
79
|
+
{
|
|
80
|
+
displayName: 'Visibility',
|
|
81
|
+
name: 'visibility',
|
|
82
|
+
type: 'options',
|
|
83
|
+
options: [
|
|
84
|
+
{ name: 'Public', value: 'PUBLIC' },
|
|
85
|
+
{ name: 'Private', value: 'PRIVATE' },
|
|
86
|
+
],
|
|
87
|
+
default: 'PUBLIC',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
// ── Get Many Fields ──
|
|
92
|
+
{
|
|
93
|
+
displayName: 'Return All',
|
|
94
|
+
name: 'returnAll',
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
default: false,
|
|
97
|
+
description: 'Whether to return all results or only up to a given limit',
|
|
98
|
+
displayOptions: { show: { resource: ['form'], operation: ['getMany'] } },
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
displayName: 'Limit',
|
|
102
|
+
name: 'limit',
|
|
103
|
+
type: 'number',
|
|
104
|
+
default: 50,
|
|
105
|
+
typeOptions: { minValue: 1, maxValue: 100 },
|
|
106
|
+
description: 'Max number of results to return',
|
|
107
|
+
displayOptions: { show: { resource: ['form'], operation: ['getMany'], returnAll: [false] } },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
displayName: 'Filters',
|
|
111
|
+
name: 'filters',
|
|
112
|
+
type: 'collection',
|
|
113
|
+
placeholder: 'Add Filter',
|
|
114
|
+
default: {},
|
|
115
|
+
displayOptions: { show: { resource: ['form'], operation: ['getMany'] } },
|
|
116
|
+
options: [
|
|
117
|
+
{ displayName: 'Search', name: 'search', type: 'string', default: '', description: 'Search by form title' },
|
|
118
|
+
{ displayName: 'Since', name: 'since', type: 'dateTime', default: '', description: 'Only return forms updated after this time (ISO 8601)' },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.responseFields = exports.responseOperations = void 0;
|
|
4
|
+
exports.responseOperations = [
|
|
5
|
+
{
|
|
6
|
+
displayName: 'Operation',
|
|
7
|
+
name: 'operation',
|
|
8
|
+
type: 'options',
|
|
9
|
+
noDataExpression: true,
|
|
10
|
+
displayOptions: { show: { resource: ['response'] } },
|
|
11
|
+
options: [
|
|
12
|
+
{ name: 'Get', value: 'get', action: 'Get a response', description: 'Get a single form response' },
|
|
13
|
+
{ name: 'Get Many', value: 'getMany', action: 'Get many responses', description: 'List form responses (paginated)' },
|
|
14
|
+
],
|
|
15
|
+
default: 'getMany',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
exports.responseFields = [
|
|
19
|
+
// ── Form ID (shared) ──
|
|
20
|
+
{
|
|
21
|
+
displayName: 'Form ID',
|
|
22
|
+
name: 'formId',
|
|
23
|
+
type: 'string',
|
|
24
|
+
required: true,
|
|
25
|
+
default: '',
|
|
26
|
+
description: 'The UUID of the form',
|
|
27
|
+
displayOptions: { show: { resource: ['response'] } },
|
|
28
|
+
},
|
|
29
|
+
// ── Response ID (Get) ──
|
|
30
|
+
{
|
|
31
|
+
displayName: 'Response ID',
|
|
32
|
+
name: 'responseId',
|
|
33
|
+
type: 'string',
|
|
34
|
+
required: true,
|
|
35
|
+
default: '',
|
|
36
|
+
description: 'The UUID of the response',
|
|
37
|
+
displayOptions: { show: { resource: ['response'], operation: ['get'] } },
|
|
38
|
+
},
|
|
39
|
+
// ── Get Many Fields ──
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Return All',
|
|
42
|
+
name: 'returnAll',
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
default: false,
|
|
45
|
+
description: 'Whether to return all results or only up to a given limit',
|
|
46
|
+
displayOptions: { show: { resource: ['response'], operation: ['getMany'] } },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
displayName: 'Limit',
|
|
50
|
+
name: 'limit',
|
|
51
|
+
type: 'number',
|
|
52
|
+
default: 50,
|
|
53
|
+
typeOptions: { minValue: 1, maxValue: 100 },
|
|
54
|
+
description: 'Max number of results to return',
|
|
55
|
+
displayOptions: { show: { resource: ['response'], operation: ['getMany'], returnAll: [false] } },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
displayName: 'Filters',
|
|
59
|
+
name: 'filters',
|
|
60
|
+
type: 'collection',
|
|
61
|
+
placeholder: 'Add Filter',
|
|
62
|
+
default: {},
|
|
63
|
+
displayOptions: { show: { resource: ['response'], operation: ['getMany'] } },
|
|
64
|
+
options: [
|
|
65
|
+
{ displayName: 'Start Date', name: 'startDate', type: 'dateTime', default: '', description: 'Only return responses submitted after this date' },
|
|
66
|
+
{ displayName: 'End Date', name: 'endDate', type: 'dateTime', default: '', description: 'Only return responses submitted before this date' },
|
|
67
|
+
{ displayName: 'Since', name: 'since', type: 'dateTime', default: '', description: 'Only return responses submitted after this time (for polling)' },
|
|
68
|
+
{
|
|
69
|
+
displayName: 'Order',
|
|
70
|
+
name: 'order',
|
|
71
|
+
type: 'options',
|
|
72
|
+
options: [
|
|
73
|
+
{ name: 'Newest First', value: 'newest_first' },
|
|
74
|
+
{ name: 'Oldest First', value: 'oldest_first' },
|
|
75
|
+
],
|
|
76
|
+
default: 'newest_first',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, IPollFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare const BASE_URL = "https://api.formfex.com";
|
|
3
|
+
export declare const API_PREFIX = "/api/v1/public";
|
|
4
|
+
export declare const MAX_PAGES = 50;
|
|
5
|
+
/** Validates a string is a UUID. Throws NodeApiError if not. */
|
|
6
|
+
export declare function validateUuid(context: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions, value: string, fieldName: string): void;
|
|
7
|
+
/** Safe URL path encoding for user-supplied IDs. */
|
|
8
|
+
export declare function safePath(id: string): string;
|
|
9
|
+
/** Make an authenticated API request to Formfex. */
|
|
10
|
+
export declare function formfexApiRequest(this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions, method: 'GET' | 'POST' | 'PATCH' | 'DELETE', endpoint: string, body?: object, query?: Record<string, string | number | boolean>): Promise<any>;
|
|
11
|
+
/** Strip internal details from error messages before showing to user. */
|
|
12
|
+
export declare function sanitizeError(message: string): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PAGES = exports.API_PREFIX = exports.BASE_URL = void 0;
|
|
4
|
+
exports.validateUuid = validateUuid;
|
|
5
|
+
exports.safePath = safePath;
|
|
6
|
+
exports.formfexApiRequest = formfexApiRequest;
|
|
7
|
+
exports.sanitizeError = sanitizeError;
|
|
8
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
9
|
+
exports.BASE_URL = 'https://api.formfex.com';
|
|
10
|
+
exports.API_PREFIX = '/api/v1/public';
|
|
11
|
+
exports.MAX_PAGES = 50;
|
|
12
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
/** Validates a string is a UUID. Throws NodeApiError if not. */
|
|
14
|
+
function validateUuid(context, value, fieldName) {
|
|
15
|
+
if (!UUID_REGEX.test(value)) {
|
|
16
|
+
throw new n8n_workflow_1.NodeApiError(context.getNode(), {
|
|
17
|
+
message: `Invalid ${fieldName}: must be a valid UUID`,
|
|
18
|
+
description: `Received: "${value}"`,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Safe URL path encoding for user-supplied IDs. */
|
|
23
|
+
function safePath(id) {
|
|
24
|
+
return encodeURIComponent(id);
|
|
25
|
+
}
|
|
26
|
+
/** Error map for friendly user-facing messages. */
|
|
27
|
+
const ERROR_MAP = {
|
|
28
|
+
401: 'Authentication failed. Check your Formfex API key.',
|
|
29
|
+
402: 'Insufficient credits. Upgrade your Formfex plan.',
|
|
30
|
+
403: 'Your API key does not have the required scope for this operation.',
|
|
31
|
+
404: 'The requested resource was not found.',
|
|
32
|
+
429: 'Rate limit exceeded. Please slow down your requests.',
|
|
33
|
+
};
|
|
34
|
+
/** Make an authenticated API request to Formfex. */
|
|
35
|
+
async function formfexApiRequest(method, endpoint, body, query) {
|
|
36
|
+
const options = {
|
|
37
|
+
method,
|
|
38
|
+
url: `${exports.BASE_URL}${exports.API_PREFIX}${endpoint}`,
|
|
39
|
+
json: true,
|
|
40
|
+
qs: query ?? {},
|
|
41
|
+
};
|
|
42
|
+
if (body && Object.keys(body).length > 0) {
|
|
43
|
+
options.body = body;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await this.helpers.requestWithAuthentication.call(this, 'formfexApi', options);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const statusCode = error?.statusCode ?? error?.response?.statusCode;
|
|
50
|
+
const friendlyMessage = statusCode ? ERROR_MAP[statusCode] : undefined;
|
|
51
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
52
|
+
message: friendlyMessage ?? `Formfex API error (${statusCode ?? 'unknown'})`,
|
|
53
|
+
description: sanitizeError(error?.message ?? ''),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Strip internal details from error messages before showing to user. */
|
|
58
|
+
function sanitizeError(message) {
|
|
59
|
+
// Remove internal paths, stack traces, and sensitive details
|
|
60
|
+
return message
|
|
61
|
+
.replace(/\/[a-zA-Z0-9/_.-]+\.(ts|js):\d+/g, '[internal]')
|
|
62
|
+
.replace(/at\s+[\w.]+\s+\(.*\)/g, '')
|
|
63
|
+
.substring(0, 500);
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-formfex",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "n8n community node for Formfex — AI-powered form builder",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"n8n": {
|
|
9
|
+
"n8nNodesApiVersion": 1,
|
|
10
|
+
"credentials": ["dist/credentials/FormfexApi.credentials.js"],
|
|
11
|
+
"nodes": [
|
|
12
|
+
"dist/nodes/Formfex/Formfex.node.js",
|
|
13
|
+
"dist/nodes/Formfex/FormfexTrigger.node.js"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"n8n-community-node-package",
|
|
18
|
+
"n8n",
|
|
19
|
+
"formfex",
|
|
20
|
+
"forms",
|
|
21
|
+
"automation",
|
|
22
|
+
"ai",
|
|
23
|
+
"form-builder"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"lint": "eslint . --ext .ts",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"n8n-workflow": "*"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"n8n-workflow": "^1.72.0",
|
|
35
|
+
"typescript": "^5.7.0",
|
|
36
|
+
"@types/node": "^22.0.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/formfex/n8n-nodes-formfex"
|
|
44
|
+
}
|
|
45
|
+
}
|