@limetech/n8n-nodes-lime 2.3.1-dev.1 → 2.5.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/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release.yml +16 -2
- package/.github/workflows/test-and-build.yml +0 -15
- package/CHANGELOG.md +35 -2
- package/nodes/errorHandling.ts +60 -0
- package/nodes/lime-crm/LimeCrmNode.node.ts +8 -0
- package/nodes/lime-crm/LimeCrmTrigger.node.ts +19 -5
- package/nodes/lime-crm/methods/getLimetypeProperties.ts +3 -1
- package/nodes/lime-crm/methods/getLimetypes.ts +2 -1
- package/nodes/lime-crm/methods/index.ts +5 -0
- package/nodes/lime-crm/methods/resourceMapping.ts +141 -0
- package/nodes/lime-crm/models/limetype.ts +18 -0
- package/nodes/lime-crm/resources/admin/index.ts +9 -4
- package/nodes/lime-crm/resources/admin/operations/getManyUsers.operation.ts +10 -2
- package/nodes/lime-crm/resources/admin/operations/getSingleUser.operation.ts +14 -15
- package/nodes/lime-crm/resources/data/index.ts +15 -6
- package/nodes/lime-crm/resources/data/operations/createSingleObject.operation.ts +25 -71
- package/nodes/lime-crm/resources/data/operations/deleteSingleObject.operation.ts +7 -2
- package/nodes/lime-crm/resources/data/operations/getManyObjects.operation.ts +6 -2
- package/nodes/lime-crm/resources/data/operations/getSingleFile.operation.ts +15 -3
- package/nodes/lime-crm/resources/data/operations/getSingleObject.operation.ts +15 -6
- package/nodes/lime-crm/resources/data/operations/updateSingleObject.operation.ts +41 -57
- package/nodes/lime-crm/resources/metadata/index.ts +7 -3
- package/nodes/lime-crm/resources/metadata/operations/getAllLimetypes.operation.ts +6 -2
- package/nodes/lime-crm/resources/metadata/operations/getSingleFileMetadata.operation.ts +18 -15
- package/nodes/lime-crm/resources/metadata/operations/getSingleLimetype.operation.ts +8 -3
- package/nodes/lime-crm/transport/commons.ts +34 -20
- package/nodes/lime-crm/transport/files.ts +72 -47
- package/nodes/lime-crm/transport/limeQuery.ts +2 -2
- package/nodes/lime-crm/transport/limeobjects.ts +22 -10
- package/nodes/lime-crm/transport/limetypes.ts +37 -16
- package/nodes/lime-crm/transport/users.ts +74 -38
- package/nodes/lime-crm/transport/webhooks.ts +5 -4
- package/nodes/lime-crm/utils/files.ts +27 -10
- package/nodes/lime-crm/utils/index.ts +1 -1
- package/nodes/response.ts +41 -3
- package/package.json +4 -2
- package/tests/nodes/lime-crm/methods.spec.ts +91 -0
- package/tests/nodes/lime-crm/utils.spec.ts +60 -25
- package/nodes/lime-crm/utils/propertyAdapters.ts +0 -75
- package/restore_script/README +0 -42
- package/restore_script/api_key_upload.txt +0 -0
- package/restore_script/cli.py +0 -73
- package/restore_script/download.py +0 -73
- package/restore_script/main.py +0 -19
- package/restore_script/poetry.lock +0 -162
- package/restore_script/pyproject.toml +0 -15
- package/restore_script/transfer.py +0 -41
- package/restore_script/upload.py +0 -66
- package/restore_script/utils.py +0 -42
- /package/{restore_script/api_key_download.txt → Dockerfile} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
LoggerProxy as Logger,
|
|
6
6
|
} from 'n8n-workflow';
|
|
7
7
|
import { createFile, getFileContent, getFileMetadata } from '../transport';
|
|
8
|
-
import {
|
|
8
|
+
import { APIResponse, FileAPIResponse } from '../../response';
|
|
9
9
|
import { LimetypeProperty } from '../models';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -121,7 +121,7 @@ export async function setFileProperties(
|
|
|
121
121
|
i: number,
|
|
122
122
|
fileProperties: Set<string>,
|
|
123
123
|
definedProperties: IDataObject
|
|
124
|
-
): Promise<IDataObject
|
|
124
|
+
): Promise<APIResponse<IDataObject>> {
|
|
125
125
|
for (const fileProperty of fileProperties) {
|
|
126
126
|
if (!(fileProperty in definedProperties)) continue;
|
|
127
127
|
let binaryData: IBinaryData;
|
|
@@ -146,9 +146,14 @@ export async function setFileProperties(
|
|
|
146
146
|
definedProperties[fileProperty] as string
|
|
147
147
|
);
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
if (response.success) {
|
|
150
|
+
definedProperties[fileProperty] = response.data.id;
|
|
151
|
+
} else return response;
|
|
150
152
|
}
|
|
151
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
data: definedProperties,
|
|
156
|
+
};
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
/**
|
|
@@ -159,7 +164,7 @@ export async function setFileProperties(
|
|
|
159
164
|
* @param data - The record data containing property values to process
|
|
160
165
|
* @param includeFileContent - Whether to include the actual file content in the response. Defaults to `false`
|
|
161
166
|
*
|
|
162
|
-
* @returns A {@link
|
|
167
|
+
* @returns A {@link FileAPIResponse} object containing updated JSON data and, if requested, the associated binary files.
|
|
163
168
|
*
|
|
164
169
|
* @public
|
|
165
170
|
* @group Utils
|
|
@@ -169,7 +174,7 @@ export async function processFileResponse<T extends Record<string, unknown>>(
|
|
|
169
174
|
fileProperties: Set<string>,
|
|
170
175
|
data: T,
|
|
171
176
|
includeFileContent: boolean = false
|
|
172
|
-
): Promise<
|
|
177
|
+
): Promise<FileAPIResponse<T>> {
|
|
173
178
|
let updatedData = { ...data };
|
|
174
179
|
const binaryData: Record<string, IBinaryData> = {};
|
|
175
180
|
for (const fileProperty of fileProperties) {
|
|
@@ -178,21 +183,33 @@ export async function processFileResponse<T extends Record<string, unknown>>(
|
|
|
178
183
|
nodeContext,
|
|
179
184
|
data[fileProperty] as string
|
|
180
185
|
);
|
|
186
|
+
if (!fileMetadataResponse.success)
|
|
187
|
+
return {
|
|
188
|
+
json: fileMetadataResponse,
|
|
189
|
+
};
|
|
181
190
|
|
|
182
191
|
updatedData = {
|
|
183
192
|
...updatedData,
|
|
184
|
-
[fileProperty]: fileMetadataResponse,
|
|
193
|
+
[fileProperty]: fileMetadataResponse.data,
|
|
185
194
|
};
|
|
186
195
|
|
|
187
196
|
if (includeFileContent) {
|
|
188
|
-
|
|
197
|
+
const fileContentResponse = await getFileContent(
|
|
189
198
|
nodeContext,
|
|
190
|
-
fileMetadataResponse.id
|
|
199
|
+
fileMetadataResponse.data.id
|
|
191
200
|
);
|
|
201
|
+
if (fileContentResponse.success) {
|
|
202
|
+
binaryData[fileProperty] = fileContentResponse.data;
|
|
203
|
+
} else {
|
|
204
|
+
return { json: fileContentResponse };
|
|
205
|
+
}
|
|
192
206
|
}
|
|
193
207
|
}
|
|
194
208
|
return {
|
|
195
|
-
json:
|
|
209
|
+
json: {
|
|
210
|
+
success: true,
|
|
211
|
+
data: updatedData,
|
|
212
|
+
},
|
|
196
213
|
binary: binaryData,
|
|
197
214
|
};
|
|
198
215
|
}
|
package/nodes/response.ts
CHANGED
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import { IBinaryData } from 'n8n-workflow';
|
|
2
|
+
import { ErrorResponse, WorkflowErrorContext } from './errorHandling';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wrapper for successful response
|
|
6
|
+
*/
|
|
7
|
+
export type SuccessResponse<T> = {
|
|
8
|
+
success: true;
|
|
9
|
+
data: T;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generic wrapper used for describing the data we are returning from workflow
|
|
14
|
+
* execution for the user
|
|
15
|
+
*/
|
|
16
|
+
export type WorkflowResponse<T> = T | { error: WorkflowErrorContext };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generic wrapper used for describing the data we are getting from the
|
|
20
|
+
* external API layer
|
|
21
|
+
*/
|
|
22
|
+
export type APIResponse<T> = SuccessResponse<T> | ErrorResponse;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Response object that includes both JSON data and optional binary file content.
|
|
26
|
+
* Works in communication layer
|
|
27
|
+
*
|
|
28
|
+
* @typeParam T - The shape of the JSON data returned in the response
|
|
29
|
+
* @property json - The {@link APIResponse} object, containing structured JSON data.
|
|
30
|
+
* @property binary - An optional record of binary file data
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
* @group Response
|
|
34
|
+
*/
|
|
35
|
+
export type FileAPIResponse<T> = {
|
|
36
|
+
json: APIResponse<T>;
|
|
37
|
+
binary?: Record<string, IBinaryData>;
|
|
38
|
+
};
|
|
2
39
|
|
|
3
40
|
/**
|
|
4
41
|
* Response object that includes both JSON data and optional binary file content.
|
|
42
|
+
* Works in user interface layer
|
|
5
43
|
*
|
|
6
44
|
* @typeParam T - The shape of the JSON data returned in the response
|
|
7
|
-
* @property json - The
|
|
45
|
+
* @property json - The {@link WorflowResponse} object, containing structured JSON data.
|
|
8
46
|
* @property binary - An optional record of binary file data
|
|
9
47
|
*
|
|
10
48
|
* @public
|
|
11
49
|
* @group Response
|
|
12
50
|
*/
|
|
13
|
-
export type
|
|
14
|
-
json: T
|
|
51
|
+
export type WorkflowFileResponse<T> = {
|
|
52
|
+
json: WorkflowResponse<T>;
|
|
15
53
|
binary?: Record<string, IBinaryData>;
|
|
16
54
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@limetech/n8n-nodes-lime",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "n8n node to connect to Lime CRM",
|
|
5
|
-
"license": "MIT",
|
|
6
5
|
"main": "nodes/index.ts",
|
|
7
6
|
"scripts": {
|
|
8
7
|
"build": "tsc && copyfiles \"nodes/**/*.svg\" dist",
|
|
@@ -63,5 +62,8 @@
|
|
|
63
62
|
},
|
|
64
63
|
"peerDependencies": {
|
|
65
64
|
"n8n-workflow": "^1.109.0"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
66
68
|
}
|
|
67
69
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCreateMappingColumns,
|
|
3
|
+
getUpdateMappingColumns,
|
|
4
|
+
LimetypeProperty,
|
|
5
|
+
SuccessResponse,
|
|
6
|
+
} from '../../../nodes';
|
|
7
|
+
import { ILoadOptionsFunctions } from 'n8n-workflow';
|
|
8
|
+
import * as transport from '../../../nodes/lime-crm/transport';
|
|
9
|
+
|
|
10
|
+
const mockILoadOptionFunctions = {
|
|
11
|
+
getNodeParameter: jest.fn().mockReturnValue('company'),
|
|
12
|
+
} as unknown as ILoadOptionsFunctions;
|
|
13
|
+
|
|
14
|
+
const propertiesResponseMock = {
|
|
15
|
+
success: true,
|
|
16
|
+
data: [
|
|
17
|
+
{ name: 'p1', localname: 'Property 1', type: 'yesno', required: true },
|
|
18
|
+
{
|
|
19
|
+
name: 'p2',
|
|
20
|
+
localname: 'Property 2',
|
|
21
|
+
type: 'string',
|
|
22
|
+
required: false,
|
|
23
|
+
length: 2137,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'p3',
|
|
27
|
+
localname: 'Property 3',
|
|
28
|
+
type: 'option',
|
|
29
|
+
required: true,
|
|
30
|
+
options: [
|
|
31
|
+
{ key: 'op1', text: 'Option 1', inactive: false },
|
|
32
|
+
{ key: 'op2', text: 'Option 2', inactive: false },
|
|
33
|
+
{ key: 'op3', text: 'Option 3', inactive: true },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
} as SuccessResponse<LimetypeProperty[]>;
|
|
38
|
+
|
|
39
|
+
describe('resourceMapping', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.spyOn(transport, 'getProperties').mockResolvedValue(
|
|
42
|
+
propertiesResponseMock
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
it('gets valid resource mapping for create', async () => {
|
|
46
|
+
const columns = await getCreateMappingColumns.call(
|
|
47
|
+
mockILoadOptionFunctions
|
|
48
|
+
);
|
|
49
|
+
const property1 = columns.fields.find((p) => p.id == 'p1')!;
|
|
50
|
+
|
|
51
|
+
expect(property1.displayName).toBe('Property 1 [yesno]');
|
|
52
|
+
expect(property1.required).toBe(true);
|
|
53
|
+
expect(property1.defaultMatch).toBe(false);
|
|
54
|
+
expect(property1.display).toBe(true);
|
|
55
|
+
expect(property1.type).toBe('boolean');
|
|
56
|
+
expect(property1.options).toBe(undefined);
|
|
57
|
+
|
|
58
|
+
const property2 = columns.fields.find((p) => p.id == 'p2')!;
|
|
59
|
+
expect(property2.displayName).toBe('Property 2 [string(2137)]');
|
|
60
|
+
expect(property2.required).toBe(false);
|
|
61
|
+
expect(property2.defaultMatch).toBe(false);
|
|
62
|
+
expect(property2.display).toBe(true);
|
|
63
|
+
expect(property2.type).toBe('string');
|
|
64
|
+
expect(property2.options).toBe(undefined);
|
|
65
|
+
|
|
66
|
+
const property3 = columns.fields.find((p) => p.id == 'p3')!;
|
|
67
|
+
expect(property3.displayName).toBe('Property 3 [option]');
|
|
68
|
+
expect(property3.required).toBe(true);
|
|
69
|
+
expect(property3.defaultMatch).toBe(false);
|
|
70
|
+
expect(property3.display).toBe(true);
|
|
71
|
+
expect(property3.type).toBe('options');
|
|
72
|
+
|
|
73
|
+
const options = property3.options!;
|
|
74
|
+
expect(options).toContainEqual({
|
|
75
|
+
value: 'op1',
|
|
76
|
+
name: 'Option 1',
|
|
77
|
+
});
|
|
78
|
+
expect(options).toContainEqual({
|
|
79
|
+
value: 'op2',
|
|
80
|
+
name: 'Option 2',
|
|
81
|
+
});
|
|
82
|
+
expect(options.length).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
it('gets proper required values for update', async () => {
|
|
85
|
+
const columns = await getUpdateMappingColumns.call(
|
|
86
|
+
mockILoadOptionFunctions
|
|
87
|
+
);
|
|
88
|
+
const requiredValues = columns.fields.map((p) => p.required);
|
|
89
|
+
expect(requiredValues).toEqual([false, false, false]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
jest.mock('../../../nodes/lime-crm/transport', () => ({
|
|
2
2
|
createFile: jest.fn().mockResolvedValue({
|
|
3
|
-
|
|
3
|
+
success: true,
|
|
4
|
+
data: { id: 1 },
|
|
4
5
|
}),
|
|
5
6
|
getFileMetadata: jest.fn().mockResolvedValue({
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
success: true,
|
|
8
|
+
data: {
|
|
9
|
+
id: '123',
|
|
10
|
+
name: 'file.txt',
|
|
11
|
+
},
|
|
12
|
+
}),
|
|
13
|
+
getFileContent: jest.fn().mockResolvedValue({
|
|
14
|
+
success: true,
|
|
15
|
+
data: 'some binary data',
|
|
8
16
|
}),
|
|
9
|
-
getFileContent: jest.fn().mockResolvedValue('some binary data'),
|
|
10
17
|
getTasks: jest.fn(),
|
|
11
18
|
}));
|
|
12
19
|
|
|
@@ -63,10 +70,30 @@ describe('files', () => {
|
|
|
63
70
|
|
|
64
71
|
describe('getFilePropertiesNames', () => {
|
|
65
72
|
const limetypes = [
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
{
|
|
74
|
+
name: 'company',
|
|
75
|
+
type: 'belongsto',
|
|
76
|
+
localname: 'company',
|
|
77
|
+
required: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'name',
|
|
81
|
+
type: 'string',
|
|
82
|
+
localname: 'name',
|
|
83
|
+
required: false,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'document',
|
|
87
|
+
type: 'file',
|
|
88
|
+
localname: 'document',
|
|
89
|
+
required: false,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'photo',
|
|
93
|
+
type: 'file',
|
|
94
|
+
localname: 'photo',
|
|
95
|
+
required: false,
|
|
96
|
+
},
|
|
70
97
|
];
|
|
71
98
|
it('returns all file property names', async () => {
|
|
72
99
|
const result = getFilePropertiesNames(limetypes);
|
|
@@ -112,8 +139,10 @@ describe('files', () => {
|
|
|
112
139
|
new Set(['document']),
|
|
113
140
|
definedProperties
|
|
114
141
|
);
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
if (result.success) {
|
|
144
|
+
expect(result.data.document).toBe(1);
|
|
145
|
+
}
|
|
117
146
|
});
|
|
118
147
|
|
|
119
148
|
it('sets file ID if assertBinaryData throws an error', async () => {
|
|
@@ -127,7 +156,10 @@ describe('files', () => {
|
|
|
127
156
|
new Set(['document']),
|
|
128
157
|
definedProperties
|
|
129
158
|
);
|
|
130
|
-
expect(result.
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
expect(result.data.document).toBe(2);
|
|
162
|
+
}
|
|
131
163
|
});
|
|
132
164
|
});
|
|
133
165
|
|
|
@@ -141,33 +173,36 @@ describe('files', () => {
|
|
|
141
173
|
|
|
142
174
|
it('returns file metadata without file content', async () => {
|
|
143
175
|
const data = { document: 1 };
|
|
144
|
-
const result = await processFileResponse(
|
|
176
|
+
const result = await processFileResponse<{ document: number }>(
|
|
145
177
|
mockNodeContext as any,
|
|
146
178
|
new Set(['document']),
|
|
147
179
|
data
|
|
148
180
|
);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
181
|
+
expect(result.json.success).toBe(true);
|
|
182
|
+
if (result.json.success) {
|
|
183
|
+
expect(result.json.data.document).toEqual({
|
|
184
|
+
id: '123',
|
|
185
|
+
name: 'file.txt',
|
|
186
|
+
});
|
|
187
|
+
expect(result.binary).toEqual({});
|
|
188
|
+
}
|
|
155
189
|
});
|
|
156
190
|
|
|
157
191
|
it('returns file metadata with file content', async () => {
|
|
158
192
|
const data = { document: 1 };
|
|
159
|
-
const result = await processFileResponse(
|
|
193
|
+
const result = await processFileResponse<{ document: number }>(
|
|
160
194
|
mockNodeContext as any,
|
|
161
195
|
new Set(['document']),
|
|
162
196
|
data,
|
|
163
197
|
true
|
|
164
198
|
);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
if (result.json.success) {
|
|
200
|
+
expect(result.json.data.document).toEqual({
|
|
201
|
+
id: '123',
|
|
202
|
+
name: 'file.txt',
|
|
203
|
+
});
|
|
204
|
+
expect(result.binary?.document).toEqual('some binary data');
|
|
205
|
+
}
|
|
171
206
|
});
|
|
172
207
|
});
|
|
173
208
|
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Set of helper functions, classes and types used for adapting the data from
|
|
3
|
-
* N8N's simple fields to Lime CRM format
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { LimetypeProperty } from '../models';
|
|
7
|
-
import { YES_NO_PROPERTY_TYPE, PropertyTypeMap } from '../models';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Base inteface for Property Adapter class
|
|
11
|
-
*/
|
|
12
|
-
interface PropertyAdapter<T> {
|
|
13
|
-
adapt(value: string): T | string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Adapter which takes a string from user input and parses it into boolean to
|
|
18
|
-
* conform to Lime CRM's YesNo type
|
|
19
|
-
*/
|
|
20
|
-
class YesNoPropertyAdapter implements PropertyAdapter<boolean> {
|
|
21
|
-
adapt(value: string): boolean | string {
|
|
22
|
-
if (value == 'true') {
|
|
23
|
-
return true;
|
|
24
|
-
} else if (value == 'false') {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Factory class which returns the proper adapter based on data type
|
|
33
|
-
* @param propertyType
|
|
34
|
-
*/
|
|
35
|
-
function getPropertyAdapter(
|
|
36
|
-
propertyType: string
|
|
37
|
-
): PropertyAdapter<PropertyTypeMap[keyof PropertyTypeMap]> | null {
|
|
38
|
-
if (propertyType == YES_NO_PROPERTY_TYPE) {
|
|
39
|
-
return new YesNoPropertyAdapter();
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Main function which takes the value from simple fields mode and the type of
|
|
46
|
-
* property and parses it accordingly to what Lime CRM expects
|
|
47
|
-
* @param propertyValue - user input, always string
|
|
48
|
-
* @param propertyType - type of property from Lime CRM
|
|
49
|
-
*
|
|
50
|
-
* @public
|
|
51
|
-
* @group Utils
|
|
52
|
-
*/
|
|
53
|
-
export function adaptProperty(propertyValue: string, propertyType: string) {
|
|
54
|
-
const adapter = getPropertyAdapter(propertyType);
|
|
55
|
-
return adapter === null ? propertyValue : adapter.adapt(propertyValue);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Given the name of the property and the set of Limetype properties, return the
|
|
60
|
-
* property type
|
|
61
|
-
* @param propertyName - name of the property we want to get the type of
|
|
62
|
-
* @param properties - list of {@link LimetypeProperty} entries
|
|
63
|
-
*
|
|
64
|
-
* @public
|
|
65
|
-
* @group Utils
|
|
66
|
-
*/
|
|
67
|
-
export function getPropertyType(
|
|
68
|
-
propertyName: string,
|
|
69
|
-
properties: LimetypeProperty[]
|
|
70
|
-
): string {
|
|
71
|
-
return (
|
|
72
|
-
properties.find((property) => property.name === propertyName)?.type ||
|
|
73
|
-
'string'
|
|
74
|
-
);
|
|
75
|
-
}
|
package/restore_script/README
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
### n8n Scripts
|
|
2
|
-
|
|
3
|
-
Utility scripts to batch upload, download and transfer workflows as JSON data to and from an n8n instance via
|
|
4
|
-
its REST API.
|
|
5
|
-
|
|
6
|
-
=========================================================================================
|
|
7
|
-
### Features
|
|
8
|
-
- download: Fetch items workflows from n8n and store locally.
|
|
9
|
-
- upload: Push local workflows to n8n.
|
|
10
|
-
- transfer: Combine download then upload for migrating between two instances without local storage
|
|
11
|
-
- cli: Unified command line entrypoint wrapping the download and upload operations.
|
|
12
|
-
|
|
13
|
-
=========================================================================================
|
|
14
|
-
### Installation
|
|
15
|
-
```bash
|
|
16
|
-
poetry install
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
=========================================================================================
|
|
20
|
-
### Usage
|
|
21
|
-
|
|
22
|
-
#### Interactive Mode
|
|
23
|
-
```bash
|
|
24
|
-
poetry run python ./main.py
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The api_key can also be provided in files [root directory]:
|
|
28
|
-
- `api_key_download.txt` for download operations
|
|
29
|
-
- `api_key_upload.txt` for upload operations
|
|
30
|
-
|
|
31
|
-
#### CLI Mode
|
|
32
|
-
Download
|
|
33
|
-
```bash
|
|
34
|
-
poetry run python ./cli.py download --instance_url <INSTANCE_URL> --api_key <API_KEY> --folder_path <FOLDER_PATH>
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Upload
|
|
38
|
-
```bash
|
|
39
|
-
poetry run python ./cli.py upload --instance_url <INSTANCE_URL> --api_key <API_KEY> --folder_path <FOLDER_PATH>
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
The api_key can also be provided via the N8N_API_KEY environment variable.
|
|
File without changes
|
package/restore_script/cli.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from download import create_workflows_endpoint, fetch_workflows, save_workflows
|
|
7
|
-
from upload import upload_workflows
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _get_api_key(parser_args):
|
|
11
|
-
api_key = parser_args.api_key or os.getenv("N8N_API_KEY")
|
|
12
|
-
if not api_key:
|
|
13
|
-
print("Error: No api_key provided. Use --api_key argument or set N8N_API_KEY environment variable.",
|
|
14
|
-
file=sys.stderr)
|
|
15
|
-
sys.exit(1)
|
|
16
|
-
|
|
17
|
-
return api_key
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def download(instance_url, folder_path, parser_args):
|
|
21
|
-
api_key = _get_api_key(parser_args)
|
|
22
|
-
url = create_workflows_endpoint(instance_url, active=False, pinned_data=True)
|
|
23
|
-
|
|
24
|
-
if workflows:= fetch_workflows(url, api_key):
|
|
25
|
-
os.makedirs(os.path.normpath(folder_path), exist_ok=True)
|
|
26
|
-
save_workflows(folder_path, workflows)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def upload(instance_url, folder_path, parser_args):
|
|
30
|
-
api_key = _get_api_key(parser_args)
|
|
31
|
-
url = f"{instance_url}/api/v1/workflows"
|
|
32
|
-
|
|
33
|
-
upload_workflows(
|
|
34
|
-
url=url, api_key=api_key, folder_path=os.path.normpath(folder_path))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def download_cmd(subparsers):
|
|
38
|
-
download_parser = subparsers.add_parser("download", help="Download workflows from an n8n instance")
|
|
39
|
-
download_parser.add_argument("--instance_url", required=True,
|
|
40
|
-
help="n8n instance URL (e.g., https://n8n.example.com)")
|
|
41
|
-
download_parser.add_argument("--folder_path", required=True,
|
|
42
|
-
help="Path to save downloaded workflows")
|
|
43
|
-
download_parser.add_argument("--api_key",
|
|
44
|
-
help="Your n8n API key the default is read from N8N_API_KEY env variable")
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def upload_cmd(subparsers):
|
|
48
|
-
upload_parser = subparsers.add_parser("upload", help="Upload workflows to an n8n instance")
|
|
49
|
-
upload_parser.add_argument("--instance_url", required=True,
|
|
50
|
-
help="n8n instance URL (e.g., https://n8n.example.com)")
|
|
51
|
-
upload_parser.add_argument("--folder_path", required=True,
|
|
52
|
-
help="Path to the folder containing workflows to upload")
|
|
53
|
-
upload_parser.add_argument("--api_key",
|
|
54
|
-
help="Your n8n API key the default is read from N8N_API_KEY env variable")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def main():
|
|
58
|
-
parser = argparse.ArgumentParser(description="A simple CLI tool for n8n workflows restore.")
|
|
59
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
60
|
-
|
|
61
|
-
download_cmd(subparsers)
|
|
62
|
-
upload_cmd(subparsers)
|
|
63
|
-
|
|
64
|
-
args = parser.parse_args()
|
|
65
|
-
|
|
66
|
-
if args.command == "download":
|
|
67
|
-
download(parser_args=args, instance_url=args.instance_url, folder_path=args.folder_path)
|
|
68
|
-
elif args.command == "upload":
|
|
69
|
-
upload(parser_args=args, instance_url=args.instance_url, folder_path=args.folder_path)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if __name__ == "__main__":
|
|
73
|
-
main()
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import os
|
|
3
|
-
|
|
4
|
-
import requests
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
from utils import get_api_key, create_headers
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def main():
|
|
11
|
-
print("n8n Workflow Downloader")
|
|
12
|
-
instance_url = input("Enter n8n instance URL (e.g., https://n8n.example.com): ").strip().rstrip("/")
|
|
13
|
-
api_key = input("Enter your n8n API key: ").strip() or get_api_key("api_key_download.txt")
|
|
14
|
-
active = input("Do you want to download only active workflows? (y/n, default n): ").strip().lower() == 'y'
|
|
15
|
-
pinned_data = input("Do you want to download pinned data also? (y/n, default n): ").strip().lower() == 'y'
|
|
16
|
-
|
|
17
|
-
url = create_workflows_endpoint(instance_url, active, pinned_data)
|
|
18
|
-
if workflows:= fetch_workflows(url, api_key):
|
|
19
|
-
folder_path = create_folder(folder_path="./workflows/", instance_url=instance_url)
|
|
20
|
-
save_workflows(folder_path, workflows)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def create_workflows_endpoint(instance_url: str, active: bool, pinned_data: bool):
|
|
24
|
-
base_url = f"{instance_url}/api/v1/workflows"
|
|
25
|
-
active = "true" if active else "false"
|
|
26
|
-
pinned_data = "true" if not pinned_data else "false"
|
|
27
|
-
|
|
28
|
-
return f"{base_url}?active={active}&excludePinnedData={pinned_data}"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def fetch_workflows(url, api_key):
|
|
32
|
-
headers = create_headers(api_key)
|
|
33
|
-
workflows = []
|
|
34
|
-
next_cursor = None
|
|
35
|
-
while True:
|
|
36
|
-
full_url = f"{url}&limit=100" + (f"&cursor={next_cursor}" if next_cursor else "")
|
|
37
|
-
try:
|
|
38
|
-
resp = requests.get(full_url, headers=headers)
|
|
39
|
-
resp.raise_for_status()
|
|
40
|
-
except requests.RequestException as e:
|
|
41
|
-
print(f"Error: {e}")
|
|
42
|
-
return []
|
|
43
|
-
|
|
44
|
-
data = resp.json()
|
|
45
|
-
workflows.extend(data.get("data", []))
|
|
46
|
-
next_cursor = data.get("nextCursor")
|
|
47
|
-
if not next_cursor:
|
|
48
|
-
break
|
|
49
|
-
|
|
50
|
-
return workflows
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def create_folder(folder_path, instance_url):
|
|
54
|
-
name = (f"{datetime.datetime.now().strftime('%Y_%m_%d-%H_%M')}-"
|
|
55
|
-
f"{instance_url.replace('https://', '').replace('http://', '').replace('/', '_')}"
|
|
56
|
-
)
|
|
57
|
-
path = os.path.join(folder_path, name)
|
|
58
|
-
os.makedirs(path, exist_ok=True)
|
|
59
|
-
|
|
60
|
-
return path
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def save_workflows(folder_path, workflows):
|
|
64
|
-
for workflow in workflows:
|
|
65
|
-
filename = f"{workflow['name']}.json"
|
|
66
|
-
with open(f"{folder_path}/{filename}", "w") as f:
|
|
67
|
-
json.dump(workflow, f, indent=2)
|
|
68
|
-
print(f"{filename} - saved")
|
|
69
|
-
print(f"All workflows has been saved to {folder_path}")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if __name__ == "__main__":
|
|
73
|
-
main()
|
package/restore_script/main.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from utils import select_option
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def main():
|
|
5
|
-
options = ["Download", "Upload", "Transfer"]
|
|
6
|
-
selected = select_option(options)
|
|
7
|
-
match selected:
|
|
8
|
-
case "Download":
|
|
9
|
-
from download import main
|
|
10
|
-
case "Upload":
|
|
11
|
-
from upload import main
|
|
12
|
-
case "Transfer":
|
|
13
|
-
from transfer import main
|
|
14
|
-
|
|
15
|
-
main()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if __name__ == "__main__":
|
|
19
|
-
main()
|