Skip to content

Commit f439b15

Browse files
committed
initial commit
1 parent 77a0fd8 commit f439b15

File tree

6 files changed

+8005
-82
lines changed

6 files changed

+8005
-82
lines changed

credentials/AitableApi.credentials.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
IAuthenticateGeneric,
3+
ICredentialTestRequest,
4+
ICredentialType,
5+
INodeProperties,
6+
} from 'n8n-workflow';
7+
8+
export class AitableApi implements ICredentialType {
9+
name = 'aitableApi';
10+
displayName = 'Aitable API';
11+
documentationUrl = 'https://developers.aitable.ai/api/introduction';
12+
properties: INodeProperties[] = [
13+
{
14+
displayName: 'API Token',
15+
name: 'apiToken',
16+
type: 'string',
17+
typeOptions: {
18+
password: true,
19+
},
20+
default: '',
21+
description: 'API Token for Aitable authentication. You can get your API Token from your Aitable account.',
22+
required: true,
23+
},
24+
];
25+
26+
authenticate: IAuthenticateGeneric = {
27+
type: 'generic',
28+
properties: {
29+
headers: {
30+
Authorization: '={{ "Bearer " + $credentials.apiToken }}',
31+
},
32+
},
33+
};
34+
35+
// The block below tells how this credential can be tested
36+
test: ICredentialTestRequest = {
37+
request: {
38+
baseURL: 'https://aitable.ai',
39+
url: '/fusion/v2/spaces',
40+
method: 'GET',
41+
},
42+
};
43+
}

nodes/AitableNode/Aitable.node.ts

+241
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import type {
2+
IExecuteFunctions,
3+
INodeExecutionData,
4+
INodeType,
5+
INodeTypeDescription,
6+
} from 'n8n-workflow';
7+
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
8+
9+
export class AitableNode implements INodeType {
10+
description: INodeTypeDescription = {
11+
displayName: 'Aitable.ai',
12+
name: 'aitable',
13+
group: ['transform'],
14+
version: 1,
15+
description: 'Interact with Aitable.ai API',
16+
defaults: {
17+
name: 'Aitable',
18+
},
19+
inputs: [NodeConnectionType.Main],
20+
outputs: [NodeConnectionType.Main],
21+
credentials: [
22+
{
23+
name: 'aitableApi',
24+
required: true,
25+
},
26+
],
27+
properties: [
28+
{
29+
displayName: 'Operation',
30+
name: 'operation',
31+
type: 'options',
32+
noDataExpression: true,
33+
options: [
34+
{
35+
name: 'Search Nodes',
36+
value: 'searchNodes',
37+
description: 'Search for nodes (datasheets, folders, etc.) in a space',
38+
},
39+
],
40+
default: 'searchNodes',
41+
},
42+
// Space ID Field
43+
{
44+
displayName: 'Space ID',
45+
name: 'spaceId',
46+
type: 'string',
47+
default: '',
48+
required: true,
49+
description: 'ID of the space (e.g., spcX9P2xUcKst)',
50+
},
51+
// Node Type Field - Shows when Search Nodes operation is selected
52+
{
53+
displayName: 'Node Type',
54+
name: 'nodeType',
55+
type: 'options',
56+
options: [
57+
{
58+
name: 'Datasheet',
59+
value: 'Datasheet',
60+
},
61+
{
62+
name: 'Folder',
63+
value: 'Folder',
64+
},
65+
{
66+
name: 'Form',
67+
value: 'Form',
68+
},
69+
{
70+
name: 'Dashboard',
71+
value: 'Dashboard',
72+
},
73+
{
74+
name: 'Mirror',
75+
value: 'Mirror',
76+
},
77+
],
78+
default: 'Datasheet',
79+
description: 'Type of node to search for',
80+
displayOptions: {
81+
show: {
82+
operation: [
83+
'searchNodes',
84+
],
85+
},
86+
},
87+
},
88+
// Query Field - Optional filter for Search Nodes
89+
{
90+
displayName: 'Query',
91+
name: 'query',
92+
type: 'string',
93+
default: '',
94+
required: false,
95+
description: 'Search nodes by name - partial matches supported',
96+
displayOptions: {
97+
show: {
98+
operation: [
99+
'searchNodes',
100+
],
101+
},
102+
},
103+
},
104+
// Permissions Field - Optional filter for Search Nodes
105+
{
106+
displayName: 'Permissions',
107+
name: 'permissions',
108+
type: 'multiOptions',
109+
options: [
110+
{
111+
name: 'Manager (0)',
112+
value: '0',
113+
description: 'Full management permissions',
114+
},
115+
{
116+
name: 'Editor (1)',
117+
value: '1',
118+
description: 'Can edit but not manage',
119+
},
120+
{
121+
name: 'Update-only (2)',
122+
value: '2',
123+
description: 'Can add and edit records but not delete',
124+
},
125+
{
126+
name: 'Read-only (3)',
127+
value: '3',
128+
description: 'Can only view data',
129+
},
130+
],
131+
default: ['0', '1', '2', '3'],
132+
description: 'Filter nodes by permission levels',
133+
displayOptions: {
134+
show: {
135+
operation: [
136+
'searchNodes',
137+
],
138+
},
139+
},
140+
},
141+
],
142+
};
143+
144+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
145+
const items = this.getInputData();
146+
const returnData: INodeExecutionData[] = [];
147+
148+
// For each input item
149+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
150+
try {
151+
const operation = this.getNodeParameter('operation', itemIndex) as string;
152+
const spaceId = this.getNodeParameter('spaceId', itemIndex) as string;
153+
154+
if (operation === 'searchNodes') {
155+
// Get parameters specific to searchNodes operation
156+
const nodeType = this.getNodeParameter('nodeType', itemIndex) as string;
157+
const query = this.getNodeParameter('query', itemIndex, '') as string;
158+
const permissionsArray = this.getNodeParameter('permissions', itemIndex, []) as string[];
159+
160+
// Build the permissions query parameter
161+
let permissionsParam = '';
162+
if (permissionsArray.length > 0) {
163+
permissionsParam = permissionsArray.join(',');
164+
}
165+
166+
// Prepare query parameters
167+
const queryParams: {
168+
type?: string;
169+
query?: string;
170+
permissions?: string;
171+
} = {
172+
type: nodeType,
173+
};
174+
175+
if (query) {
176+
queryParams.query = query;
177+
}
178+
179+
if (permissionsParam) {
180+
queryParams.permissions = permissionsParam;
181+
}
182+
183+
// Make API request to search nodes
184+
const endpoint = `/fusion/v2/spaces/${spaceId}/nodes`;
185+
const response = await this.helpers.httpRequestWithAuthentication.call(
186+
this,
187+
'aitableApi',
188+
{
189+
method: 'GET',
190+
url: endpoint,
191+
qs: queryParams,
192+
},
193+
);
194+
195+
// Process response
196+
if (response.success && response.data && response.data.nodes) {
197+
const responseData = response.data.nodes;
198+
199+
// Return data for each node found
200+
for (const node of responseData) {
201+
returnData.push({
202+
json: node,
203+
pairedItem: itemIndex,
204+
});
205+
}
206+
} else {
207+
// If no nodes were found or there was an issue with the response
208+
returnData.push({
209+
json: {
210+
success: false,
211+
message: 'No data returned or error occurred',
212+
response,
213+
},
214+
pairedItem: itemIndex,
215+
});
216+
}
217+
}
218+
} catch (error) {
219+
if (this.continueOnFail()) {
220+
returnData.push({
221+
json: {
222+
error: error.message,
223+
},
224+
pairedItem: itemIndex,
225+
});
226+
} else {
227+
// Add more context to the error
228+
if (error.context) {
229+
error.context.itemIndex = itemIndex;
230+
throw error;
231+
}
232+
throw new NodeOperationError(this.getNode(), error, {
233+
itemIndex,
234+
});
235+
}
236+
}
237+
}
238+
239+
return [returnData];
240+
}
241+
}

nodes/ExampleNode/ExampleNode.node.ts

-76
This file was deleted.

0 commit comments

Comments
 (0)