@limetech/n8n-nodes-lime 3.11.0-dev.1 → 3.11.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/credentials/LimeCrmApi.credentials.ts +1 -0
  3. package/credentials/LimeFormsApi.credentials.ts +1 -18
  4. package/credentials/LimeMarketingApi.credentials.ts +92 -0
  5. package/credentials/assets/lime-crm.svg +1 -0
  6. package/credentials/index.ts +1 -0
  7. package/dist/credentials/LimeCrmApi.credentials.d.ts +1 -0
  8. package/dist/credentials/LimeCrmApi.credentials.js +1 -0
  9. package/dist/credentials/LimeCrmApi.credentials.js.map +1 -1
  10. package/dist/credentials/LimeFormsApi.credentials.d.ts +1 -0
  11. package/dist/credentials/LimeFormsApi.credentials.js +1 -15
  12. package/dist/credentials/LimeFormsApi.credentials.js.map +1 -1
  13. package/dist/credentials/LimeMarketingApi.credentials.d.ts +9 -0
  14. package/dist/credentials/LimeMarketingApi.credentials.js +64 -0
  15. package/dist/credentials/LimeMarketingApi.credentials.js.map +1 -0
  16. package/dist/credentials/assets/lime-crm.svg +1 -0
  17. package/dist/credentials/index.d.ts +1 -0
  18. package/dist/credentials/index.js +1 -0
  19. package/dist/credentials/index.js.map +1 -1
  20. package/dist/nodes/index.d.ts +1 -0
  21. package/dist/nodes/index.js +1 -0
  22. package/dist/nodes/index.js.map +1 -1
  23. package/dist/nodes/lime-crm/assets/lime-crm.svg +0 -0
  24. package/dist/nodes/lime-crm/resources/data/index.js +0 -4
  25. package/dist/nodes/lime-crm/resources/data/index.js.map +1 -1
  26. package/dist/nodes/lime-crm/resources/data/operations/index.d.ts +0 -1
  27. package/dist/nodes/lime-crm/resources/data/operations/index.js +1 -2
  28. package/dist/nodes/lime-crm/resources/data/operations/index.js.map +1 -1
  29. package/dist/nodes/lime-crm/transport/commons.d.ts +2 -2
  30. package/dist/nodes/lime-crm/transport/commons.js +8 -7
  31. package/dist/nodes/lime-crm/transport/commons.js.map +1 -1
  32. package/dist/nodes/lime-forms/LimeFormsTrigger.node.js +22 -9
  33. package/dist/nodes/lime-forms/LimeFormsTrigger.node.js.map +1 -1
  34. package/dist/nodes/lime-forms/assets/lime-crm.svg +0 -0
  35. package/dist/nodes/lime-marketing/LimeMarketing.node.d.ts +16 -0
  36. package/dist/nodes/lime-marketing/LimeMarketing.node.js +121 -0
  37. package/dist/nodes/lime-marketing/LimeMarketing.node.js.map +1 -0
  38. package/dist/nodes/lime-marketing/assets/lime-crm.svg +1 -0
  39. package/dist/nodes/lime-marketing/index.d.ts +1 -0
  40. package/dist/nodes/lime-marketing/index.js +18 -0
  41. package/dist/nodes/lime-marketing/index.js.map +1 -0
  42. package/dist/nodes/lime-marketing/methods/getTemplateMergeCodeMappingColumns.d.ts +3 -0
  43. package/dist/nodes/lime-marketing/methods/getTemplateMergeCodeMappingColumns.js +52 -0
  44. package/dist/nodes/lime-marketing/methods/getTemplateMergeCodeMappingColumns.js.map +1 -0
  45. package/dist/nodes/lime-marketing/methods/getTemplates.d.ts +2 -0
  46. package/dist/nodes/lime-marketing/methods/getTemplates.js +24 -0
  47. package/dist/nodes/lime-marketing/methods/getTemplates.js.map +1 -0
  48. package/dist/nodes/lime-marketing/methods/index.d.ts +2 -0
  49. package/dist/nodes/lime-marketing/methods/index.js +8 -0
  50. package/dist/nodes/lime-marketing/methods/index.js.map +1 -0
  51. package/dist/nodes/lime-marketing/models/constants.d.ts +9 -0
  52. package/dist/nodes/lime-marketing/models/constants.js +13 -0
  53. package/dist/nodes/lime-marketing/models/constants.js.map +1 -0
  54. package/dist/nodes/lime-marketing/models/index.d.ts +6 -0
  55. package/dist/nodes/lime-marketing/models/index.js +14 -0
  56. package/dist/nodes/lime-marketing/models/index.js.map +1 -0
  57. package/dist/nodes/lime-marketing/models/operationInputs.d.ts +36 -0
  58. package/dist/nodes/lime-marketing/models/operationInputs.js +3 -0
  59. package/dist/nodes/lime-marketing/models/operationInputs.js.map +1 -0
  60. package/dist/nodes/lime-marketing/models/template.d.ts +10 -0
  61. package/dist/nodes/lime-marketing/models/template.js +3 -0
  62. package/dist/nodes/lime-marketing/models/template.js.map +1 -0
  63. package/dist/nodes/lime-marketing/models/transactionMail.d.ts +60 -0
  64. package/dist/nodes/lime-marketing/models/transactionMail.js +3 -0
  65. package/dist/nodes/lime-marketing/models/transactionMail.js.map +1 -0
  66. package/dist/nodes/lime-marketing/models/transactionSms.d.ts +20 -0
  67. package/dist/nodes/lime-marketing/models/transactionSms.js +3 -0
  68. package/dist/nodes/lime-marketing/models/transactionSms.js.map +1 -0
  69. package/dist/nodes/lime-marketing/models/transportError.d.ts +7 -0
  70. package/dist/nodes/lime-marketing/models/transportError.js +3 -0
  71. package/dist/nodes/lime-marketing/models/transportError.js.map +1 -0
  72. package/dist/nodes/lime-marketing/resources/transactionmail/index.d.ts +6 -0
  73. package/dist/nodes/lime-marketing/resources/transactionmail/index.js +56 -0
  74. package/dist/nodes/lime-marketing/resources/transactionmail/index.js.map +1 -0
  75. package/dist/nodes/lime-marketing/resources/transactionmail/operations/send.operation.d.ts +10 -0
  76. package/dist/nodes/lime-marketing/resources/transactionmail/operations/send.operation.js +543 -0
  77. package/dist/nodes/lime-marketing/resources/transactionmail/operations/send.operation.js.map +1 -0
  78. package/dist/nodes/lime-marketing/resources/transactionsms/index.d.ts +6 -0
  79. package/dist/nodes/lime-marketing/resources/transactionsms/index.js +56 -0
  80. package/dist/nodes/lime-marketing/resources/transactionsms/index.js.map +1 -0
  81. package/dist/nodes/{lime-crm/resources/data/operations/iterateCsv.d.ts → lime-marketing/resources/transactionsms/operations/send.operation.d.ts} +3 -2
  82. package/dist/nodes/lime-marketing/resources/transactionsms/operations/send.operation.js +171 -0
  83. package/dist/nodes/lime-marketing/resources/transactionsms/operations/send.operation.js.map +1 -0
  84. package/dist/nodes/lime-marketing/transport/index.d.ts +7 -0
  85. package/dist/nodes/lime-marketing/transport/index.js +34 -0
  86. package/dist/nodes/lime-marketing/transport/index.js.map +1 -0
  87. package/dist/nodes/lime-marketing/utils/index.d.ts +8 -0
  88. package/dist/nodes/lime-marketing/utils/index.js +62 -0
  89. package/dist/nodes/lime-marketing/utils/index.js.map +1 -0
  90. package/dist/nodes/lime-marketing/utils/mergeCodes.d.ts +26 -0
  91. package/dist/nodes/lime-marketing/utils/mergeCodes.js +85 -0
  92. package/dist/nodes/lime-marketing/utils/mergeCodes.js.map +1 -0
  93. package/dist/package.json +5 -6
  94. package/dist/tsconfig.build.tsbuildinfo +1 -1
  95. package/nodes/index.ts +1 -0
  96. package/nodes/lime-crm/assets/lime-crm.svg +0 -0
  97. package/nodes/lime-crm/resources/data/index.ts +0 -4
  98. package/nodes/lime-crm/resources/data/operations/index.ts +0 -4
  99. package/nodes/lime-crm/transport/commons.ts +9 -8
  100. package/nodes/lime-forms/LimeFormsTrigger.node.ts +51 -19
  101. package/nodes/lime-forms/assets/lime-crm.svg +0 -0
  102. package/nodes/lime-marketing/LimeMarketing.node.ts +180 -0
  103. package/nodes/lime-marketing/assets/lime-crm.svg +1 -0
  104. package/nodes/lime-marketing/index.ts +1 -0
  105. package/nodes/lime-marketing/methods/getTemplateMergeCodeMappingColumns.ts +70 -0
  106. package/nodes/lime-marketing/methods/getTemplates.ts +33 -0
  107. package/nodes/lime-marketing/methods/index.ts +2 -0
  108. package/nodes/lime-marketing/models/constants.ts +12 -0
  109. package/nodes/lime-marketing/models/index.ts +32 -0
  110. package/nodes/lime-marketing/models/operationInputs.ts +40 -0
  111. package/nodes/lime-marketing/models/template.ts +10 -0
  112. package/nodes/lime-marketing/models/transactionMail.ts +65 -0
  113. package/nodes/lime-marketing/models/transactionSms.ts +22 -0
  114. package/nodes/lime-marketing/models/transportError.ts +6 -0
  115. package/nodes/lime-marketing/resources/transactionmail/index.ts +22 -0
  116. package/nodes/lime-marketing/resources/transactionmail/operations/send.operation.ts +664 -0
  117. package/nodes/lime-marketing/resources/transactionsms/index.ts +22 -0
  118. package/nodes/lime-marketing/resources/transactionsms/operations/send.operation.ts +228 -0
  119. package/nodes/lime-marketing/transport/index.ts +68 -0
  120. package/nodes/lime-marketing/utils/index.ts +139 -0
  121. package/nodes/lime-marketing/utils/mergeCodes.ts +121 -0
  122. package/package.json +6 -7
  123. package/dist/nodes/lime-crm/resources/data/operations/iterateCsv.js +0 -225
  124. package/dist/nodes/lime-crm/resources/data/operations/iterateCsv.js.map +0 -1
  125. package/nodes/lime-crm/resources/data/operations/iterateCsv.ts +0 -377
package/nodes/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from './lime-crm';
3
3
  export * from './modules';
4
4
  export * from './lime-forms';
5
5
  export * from './limeHeaders';
6
+ export * from './lime-marketing';
File without changes
@@ -24,7 +24,6 @@ const moduleHandler = new N8NOperationModuleHandler([
24
24
  operations.bulkCreateManyObjects,
25
25
  operations.bulkUpdateManyObjects,
26
26
  operations.bulkCreateOrUpdateManyObjects,
27
- operations.iterateCsv,
28
27
  ]);
29
28
 
30
29
  /**
@@ -141,9 +140,6 @@ export async function dataOperations(
141
140
  );
142
141
  return result && { json: result };
143
142
  }
144
- case 'iterateCsv': {
145
- return await operations.iterateCsv.execute.call(this, i);
146
- }
147
143
  }
148
144
 
149
145
  throw new NodeOperationError(
@@ -34,7 +34,3 @@ export * as bulkUpdateManyObjects from './bulkUpdateManyObjects.operation';
34
34
  * @group Resources
35
35
  */
36
36
  export * as bulkCreateOrUpdateManyObjects from './bulkCreateOrUpdateManyObjects.operation';
37
- /**
38
- * @group Resources
39
- */
40
- export * as iterateCsv from './iterateCsv';
@@ -83,7 +83,7 @@ interface LimeAPIArguments {
83
83
  [key: string]: unknown;
84
84
  };
85
85
  json?: boolean;
86
- errorMetadata?: Record<string, unknown>;
86
+ errorMetadata?: JsonObject;
87
87
  }
88
88
 
89
89
  /**
@@ -106,7 +106,7 @@ export async function callLimeApi<T>(
106
106
  const { headers: callerHeaders, ...restRequestOptions } =
107
107
  options.requestOptions ?? {};
108
108
  const response =
109
- await nodeContext.helpers.requestWithAuthentication.call(
109
+ await nodeContext.helpers.httpRequestWithAuthentication.call(
110
110
  nodeContext,
111
111
  LIME_CRM_API_CREDENTIAL_KEY,
112
112
  {
@@ -126,15 +126,16 @@ export async function callLimeApi<T>(
126
126
  data: response,
127
127
  };
128
128
  } catch (error) {
129
- const errorBody = error?.cause?.error ?? error?.cause?.body;
129
+ const apiBody = error.context?.data;
130
130
  const errorContext: WorkflowErrorContext = {
131
- message: error instanceof Error ? error.message : String(error),
132
- status: error?.cause?.status,
133
- ...(errorBody != null && { error: errorBody }),
131
+ message: error.description
132
+ ? `${error.message}. ${error.description}`
133
+ : error.message,
134
+ status: error.httpCode ?? undefined,
134
135
  metadata: {
135
- ...options.requestOptions,
136
136
  ...options.errorMetadata,
137
- } as JsonObject,
137
+ },
138
+ ...(apiBody != null && { error: apiBody }),
138
139
  };
139
140
  return handleWorkflowError(nodeContext.getNode(), errorContext, true);
140
141
  }
@@ -22,8 +22,9 @@ import { ObservableActionType } from './types/enums/ObservableAction';
22
22
  import { ObservableType } from './types/enums/ObservableType';
23
23
  import { FORMS_API_CREDENTIALS_NAME } from '../../credentials';
24
24
  import { getWorkflowUrl } from './utils/workflow';
25
- import { verifyHmac } from '../crypto';
25
+ import { decryptSecret, encryptSecret, verifyHmac } from '../crypto';
26
26
  import { handleWorkflowError } from '../errorHandling';
27
+ import { randomBytes } from 'node:crypto';
27
28
 
28
29
  const FORMS_OBSERVABLE_WEBHOOK_NAME_PREFIX = 'N8N';
29
30
 
@@ -131,22 +132,26 @@ export class LimeFormsTrigger implements INodeType {
131
132
  },
132
133
 
133
134
  async create(this: IHookFunctions): Promise<boolean> {
134
- const credentials = await this.getCredentials(
135
- FORMS_API_CREDENTIALS_NAME
136
- );
135
+ const webhookData = this.getWorkflowStaticData('node');
136
+ // Generate a per-webhook secret, keep an encrypted copy in the
137
+ // workflow static data and only ever hand the plaintext to Lime
138
+ // Forms. The secret is decrypted on each incoming request to
139
+ // validate the signature. See {@link encryptSecret}.
140
+ const rawSecret = randomBytes(32).toString('hex');
141
+ webhookData.webhookSecret = encryptSecret(rawSecret);
142
+
137
143
  const data = {
138
144
  name: `${FORMS_OBSERVABLE_WEBHOOK_NAME_PREFIX}: ${this.getNodeParameter('name')}`,
139
145
  observableType: ObservableType.FORM,
140
146
  observableId: this.getNodeParameter('formId'),
141
147
  action: ObservableActionType.FORM_SUBMITTED,
142
148
  webhookUrl: this.getNodeWebhookUrl('default'),
143
- secret: credentials.webhookSecret as string,
149
+ secret: rawSecret,
144
150
  workflowUrl: getWorkflowUrl(
145
151
  this.getNode(),
146
152
  this.getWorkflow().id
147
153
  ),
148
154
  };
149
- const webhookData = this.getWorkflowStaticData('node');
150
155
 
151
156
  try {
152
157
  const response =
@@ -164,22 +169,41 @@ export class LimeFormsTrigger implements INodeType {
164
169
 
165
170
  return true;
166
171
  } catch (error) {
167
- // 409 Indicates duplicate exists
172
+ // 409 indicates a duplicate webhook already exists in Lime
173
+ // Forms. Its secret was set on an earlier registration and
174
+ // is unknown to us, so reusing it would resurrect the
175
+ // "signatures do not match" failures. Delete the stale
176
+ // webhook and recreate it with the freshly generated secret.
168
177
  if (error?.httpCode == '409') {
169
- Logger.info(
170
- `Webhook ${webhookData.name} already exists in Lime Forms, retrieving existing webhook ID`,
171
- { errorResponse: error.context?.data }
172
- );
173
178
  /**
174
179
  * Error response structure reference:
175
180
  * @see https://github.com/n8n-io/n8n/blob/n8n%401.120.0/packages/core/src/execution-engine/node-execution-context/utils/request-helper-functions.ts#L1280
176
181
  */
177
182
  const errorResponse = error.context?.data?.error;
178
- webhookData.id = errorResponse.id;
179
- webhookData.name = errorResponse.name.replace(
180
- `${FORMS_OBSERVABLE_WEBHOOK_NAME_PREFIX}: `,
181
- ''
183
+ Logger.info(
184
+ `Webhook ${errorResponse?.name} already exists in Lime Forms, recreating it with a fresh secret`,
185
+ { errorResponse: error.context?.data }
186
+ );
187
+
188
+ await new LimeFormsRequest<ObservableWebhookSimpleResource>(
189
+ this
190
+ ).delete(
191
+ `/api/v1/observable-webhooks/${errorResponse.id}`
182
192
  );
193
+
194
+ const response =
195
+ await new LimeFormsRequest<ObservableWebhookDetailedResource>(
196
+ this
197
+ ).post('/api/v1/observable-webhooks', data);
198
+
199
+ if (!response.success) {
200
+ throw new NodeApiError(this.getNode(), {
201
+ message:
202
+ 'Failed to create webhook in Lime Forms',
203
+ });
204
+ }
205
+
206
+ webhookData.id = response.data.id;
183
207
  return true;
184
208
  }
185
209
  throw new NodeApiError(this.getNode(), {
@@ -207,7 +231,7 @@ export class LimeFormsTrigger implements INodeType {
207
231
  }
208
232
 
209
233
  delete webhookData.id;
210
- delete webhookData.secret;
234
+ delete webhookData.webhookSecret;
211
235
 
212
236
  return true;
213
237
  },
@@ -227,9 +251,17 @@ export class LimeFormsTrigger implements INodeType {
227
251
  );
228
252
  }
229
253
 
230
- const webhookSecret = await this.getCredentials(
231
- FORMS_API_CREDENTIALS_NAME
232
- ).then((c) => c.webhookSecret as string);
254
+ const webhookData = this.getWorkflowStaticData('node');
255
+ const encryptedSecret = webhookData.webhookSecret as
256
+ | string
257
+ | undefined;
258
+ if (!encryptedSecret) {
259
+ throw new NodeOperationError(
260
+ this.getNode(),
261
+ 'Webhook is not registered: missing secret in static data'
262
+ );
263
+ }
264
+ const webhookSecret = decryptSecret(encryptedSecret);
233
265
  const isValidSignature = verifyHmac(
234
266
  webhookSecret,
235
267
  Buffer.from(JSON.stringify(body)),
File without changes
@@ -0,0 +1,180 @@
1
+ import {
2
+ IDataObject,
3
+ IExecuteFunctions,
4
+ INodeExecutionData,
5
+ INodeType,
6
+ INodeTypeDescription,
7
+ NodeApiError,
8
+ NodeConnectionTypes,
9
+ NodeOperationError,
10
+ NodePropertyTypes,
11
+ } from 'n8n-workflow';
12
+ import {
13
+ LIME_MARKETING_API_CREDENTIAL_KEY,
14
+ TRANSACTIONAL_EMAIL_RESOURCE,
15
+ TRANSACTIONAL_SMS_RESOURCE,
16
+ } from './models';
17
+ import {
18
+ transactionMailFields,
19
+ transactionMailOperations,
20
+ } from './resources/transactionmail';
21
+ import {
22
+ transactionSmsFields,
23
+ transactionSmsOperations,
24
+ } from './resources/transactionsms';
25
+ import { getTemplates, getTemplateMergeCodeMappingColumns } from './methods';
26
+ import { getBaseUrl } from './utils';
27
+
28
+ // What an OperationExecutor returns: either a raw API response object, an
29
+ // array of them, or already-wrapped n8n execution items (single or many).
30
+ // `undefined` means "no items emitted from this input".
31
+ export type OperationExecutorResult =
32
+ | IDataObject
33
+ | IDataObject[]
34
+ | INodeExecutionData
35
+ | INodeExecutionData[]
36
+ | undefined;
37
+
38
+ // Signature for a per-item operation. Receives the item index and a
39
+ // pre-resolved baseURL so we don't re-fetch credentials on every call.
40
+ export type OperationExecutor = (
41
+ this: IExecuteFunctions,
42
+ i: number,
43
+ baseURL: string
44
+ ) => Promise<OperationExecutorResult>;
45
+
46
+ const OPERATIONS: Record<string, Record<string, OperationExecutor>> = {
47
+ [TRANSACTIONAL_EMAIL_RESOURCE]: transactionMailOperations,
48
+ [TRANSACTIONAL_SMS_RESOURCE]: transactionSmsOperations,
49
+ };
50
+
51
+ function hasJsonField(
52
+ value: IDataObject | INodeExecutionData
53
+ ): value is INodeExecutionData {
54
+ return 'json' in value;
55
+ }
56
+
57
+ // Wrap one raw response into an n8n execution item, preserving `json` if
58
+ // already present and tagging `pairedItem`.
59
+ function toExecutionItem(
60
+ raw: IDataObject | INodeExecutionData,
61
+ i: number
62
+ ): INodeExecutionData {
63
+ const base = hasJsonField(raw) ? raw : { json: raw };
64
+ return { ...base, pairedItem: { item: i } };
65
+ }
66
+
67
+ // Normalize a response (single value, array, or undefined) into an array
68
+ // of execution items.
69
+ function toExecutionItems(
70
+ responseData: OperationExecutorResult,
71
+ i: number
72
+ ): INodeExecutionData[] {
73
+ if (responseData === undefined) return [];
74
+ const list = Array.isArray(responseData) ? responseData : [responseData];
75
+ return list.map((item) => toExecutionItem(item, i));
76
+ }
77
+
78
+ // Build an error-flavored execution item for the `continueOnFail` branch,
79
+ // attaching the original NodeError when available.
80
+ function buildErrorItem(error: Error, i: number): INodeExecutionData {
81
+ const nodeError =
82
+ error instanceof NodeApiError || error instanceof NodeOperationError
83
+ ? error
84
+ : undefined;
85
+ // n8n's executor overwrites `json` with `{ error: error.message }` whenever
86
+ // an item has a top-level `error` field, so anything else in `json` would
87
+ // be discarded before downstream nodes see it.
88
+ return {
89
+ json: { error: error.message },
90
+ error: nodeError,
91
+ pairedItem: { item: i },
92
+ };
93
+ }
94
+
95
+ export class LimeMarketing implements INodeType {
96
+ description: INodeTypeDescription = {
97
+ displayName: 'Lime CRM Marketing',
98
+ name: 'limeMarketing',
99
+ icon: 'file:assets/lime-crm.svg',
100
+ group: ['transform'],
101
+ version: 1,
102
+ subtitle:
103
+ '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
104
+ description:
105
+ 'Send transactional emails and SMS via the Lime CRM Marketing API',
106
+ defaults: { name: 'Lime CRM Marketing' },
107
+ inputs: [NodeConnectionTypes.Main],
108
+ outputs: [NodeConnectionTypes.Main],
109
+ credentials: [
110
+ { name: LIME_MARKETING_API_CREDENTIAL_KEY, required: true },
111
+ ],
112
+ usableAsTool: true,
113
+ properties: [
114
+ {
115
+ displayName:
116
+ 'Tip: set On Error to "Continue (using error output)" in the Settings tab to route failed sends down a separate branch. The error output contains the API message as $json.error.',
117
+ name: 'errorHandlingHint',
118
+ type: 'notice',
119
+ default: '',
120
+ },
121
+ {
122
+ displayName: 'Resource',
123
+ name: 'resource',
124
+ type: 'options' as NodePropertyTypes,
125
+ noDataExpression: true,
126
+ options: [
127
+ {
128
+ name: 'Email',
129
+ value: TRANSACTIONAL_EMAIL_RESOURCE,
130
+ description: 'Send a transactional email',
131
+ },
132
+ {
133
+ name: 'SMS',
134
+ value: TRANSACTIONAL_SMS_RESOURCE,
135
+ description: 'Send a transactional SMS',
136
+ },
137
+ ],
138
+ default: TRANSACTIONAL_EMAIL_RESOURCE,
139
+ },
140
+
141
+ ...transactionMailFields,
142
+ ...transactionSmsFields,
143
+ ],
144
+ };
145
+
146
+ methods = {
147
+ loadOptions: {
148
+ getTemplates,
149
+ },
150
+ resourceMapping: {
151
+ getTemplateMergeCodeMappingColumns,
152
+ },
153
+ };
154
+
155
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
156
+ const items = this.getInputData();
157
+ const resource = this.getNodeParameter('resource', 0) as string;
158
+ const operation = this.getNodeParameter('operation', 0) as string;
159
+ const executor = OPERATIONS[resource]?.[operation];
160
+ if (!executor) return [[]];
161
+
162
+ const baseURL = await getBaseUrl(this);
163
+ const returnData: INodeExecutionData[] = [];
164
+ for (let i = 0; i < items.length; i++) {
165
+ try {
166
+ const responseData = await executor.call(this, i, baseURL);
167
+ returnData.push(...toExecutionItems(responseData, i));
168
+ } catch (error_) {
169
+ if (!this.continueOnFail()) throw error_;
170
+ const error =
171
+ error_ instanceof Error
172
+ ? error_
173
+ : new Error(String(error_));
174
+ returnData.push(buildErrorItem(error, i));
175
+ }
176
+ }
177
+
178
+ return [returnData];
179
+ }
180
+ }
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 1024 1025" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><clipPath id="a"><path d="M0 .03h1024v1024H0z"/></clipPath><g clip-path="url(#a)"><path d="M92.399 480.888c0-257.059 208.719-465.779 465.78-465.779C815.28 15.109 1024 223.829 1024 480.888c0 133.713-55.806 271.139-193.403 385.355-166.779 138.45-390.39 157.35-736.15 134.525-102.653-6.785-117.502-24.319-64.468-63.444 166.694-123.047 62.42-217.167 62.42-456.436Z" style="fill:#bee034"/><clipPath id="b"><path d="M92.399 480.888c0-257.059 208.719-465.779 465.78-465.779C815.28 15.109 1024 223.829 1024 480.888c0 133.713-55.806 271.139-193.403 385.355-166.779 138.45-390.39 157.35-736.15 134.525-102.653-6.785-117.502-24.319-64.468-63.444 166.694-123.047 62.42-217.167 62.42-456.436Z"/></clipPath><g clip-path="url(#b)"><path d="M1023-361.59c0 784.587-711.326 1420.62-1588.79 1420.62C-1402.638-283.55 1023-1146.18 1023-361.59Z" style="fill:#a6efff;filter:blur(200px)"/></g><path d="M433.85 723.911c-24.702-12.757-47.23-29.226-66.813-48.596-9.088-9.002-23.765-8.959-32.768.129-9.001 9.087-8.916 23.763.171 32.766 22.911 22.698 49.236 41.941 78.121 56.873 11.392 5.888 25.385 1.409 31.231-9.94 5.888-11.349 1.409-25.344-9.942-31.232Zm367.266-363.98a270.163 270.163 0 0 1 25.556 78.547c2.048 12.629 13.952 21.206 26.581 19.158 12.586-2.006 21.204-13.951 19.156-26.539a316.387 316.387 0 0 0-29.907-92.029c-5.803-11.434-19.754-15.999-31.146-10.24-11.393 5.76-16 19.713-10.24 31.103Zm-51.497 315.384c-19.584 19.37-42.069 35.839-66.815 48.596-11.349 5.888-15.786 19.883-9.94 31.232 5.887 11.349 19.881 15.828 31.229 9.94 28.929-14.932 55.253-34.175 78.164-56.873 9.045-9.003 9.131-23.679.128-32.766-9.002-9.088-23.678-9.131-32.766-.129ZM517.006 213.546a274.904 274.904 0 0 1 41.343-3.114c14.036 0 27.861 1.066 41.343 3.114 12.629 1.919 24.448-6.784 26.367-19.413 1.962-12.628-6.741-24.446-19.371-26.409a322.626 322.626 0 0 0-48.339-3.627 322.201 322.201 0 0 0-48.34 3.627c-12.672 1.963-21.332 13.781-19.413 26.409 1.92 12.629 13.739 21.332 26.41 19.413Zm41.343 490.355c-40.489 0-73.385 32.853-73.385 73.342s32.896 73.385 73.385 73.385c40.489 0 73.342-32.896 73.342-73.385 0-40.489-32.853-73.342-73.342-73.342ZM290.025 438.478c4.481-27.818 13.227-54.227 25.514-78.547 5.761-11.39 1.195-25.343-10.239-31.103-11.392-5.759-25.343-1.194-31.104 10.24a313.893 313.893 0 0 0-29.908 92.029c-2.048 12.588 6.527 24.533 19.157 26.539 12.629 2.048 24.532-6.529 26.58-19.158Zm479.092 112.254c-12.501 38.527 8.618 79.955 47.102 92.456 38.528 12.502 79.956-8.618 92.457-47.104 12.501-38.525-8.619-79.953-47.103-92.455-38.527-12.501-79.955 8.619-92.456 47.103Zm-80.51-247.802c32.766 23.808 78.675 16.554 102.482-16.212 23.808-32.767 16.512-78.675-16.256-102.482-32.723-23.809-78.675-16.512-102.439 16.254-23.808 32.725-16.555 78.633 16.213 102.44Zm-260.558 0c32.766-23.807 40.063-69.715 16.254-102.44-23.807-32.766-69.715-40.063-102.482-16.254-32.766 23.807-40.02 69.715-16.255 102.482 23.806 32.766 69.758 40.02 102.483 16.212Zm-80.51 247.802c-12.501-38.484-53.929-59.604-92.415-47.103-38.526 12.502-59.645 53.93-47.102 92.455 12.501 38.486 53.929 59.606 92.413 47.104 38.528-12.501 59.605-53.929 47.104-92.456Z" style="fill:#fff"/></g></svg>
@@ -0,0 +1 @@
1
+ export * from './LimeMarketing.node';
@@ -0,0 +1,70 @@
1
+ import { ILoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';
2
+ import { limeMarketingRequest } from '../transport';
3
+ import { LoggerProxy as Logger } from 'n8n-workflow';
4
+
5
+ type TemplateVariablesResponse = {
6
+ RecipientVariables?: { Property: string; RawValue: string }[];
7
+ CustomVariables?: { Property: string; RawValue: string }[];
8
+ };
9
+
10
+ const PLACEHOLDER_ID = '__noMergeCodes__';
11
+
12
+ // True if `name` is the synthetic "no merge codes" sentinel produced for
13
+ // templates without merge codes — must be filtered out before sending.
14
+ export function isMergeCodePlaceholder(name: string): boolean {
15
+ return name === PLACEHOLDER_ID;
16
+ }
17
+
18
+ const noMergeCodesPlaceholder = {
19
+ id: PLACEHOLDER_ID,
20
+ displayName: 'No merge codes in this template',
21
+ required: false,
22
+ defaultMatch: false,
23
+ display: true,
24
+ type: 'string' as const,
25
+ canBeUsedToMatch: false,
26
+ readOnly: true,
27
+ };
28
+
29
+ // resourceMapping handler: fetch the merge codes (CustomVariables) for the
30
+ // currently-selected template and expose each as a mappable field. Returns a
31
+ // readOnly placeholder field when the template has none, since n8n's resource
32
+ // mapper requires at least one field to render.
33
+ export async function getTemplateMergeCodeMappingColumns(
34
+ this: ILoadOptionsFunctions
35
+ ): Promise<ResourceMapperFields> {
36
+ const templateId = Number(this.getNodeParameter('templateId', 0));
37
+ if (!templateId) return { fields: [noMergeCodesPlaceholder] };
38
+
39
+ const tagsResponse = await limeMarketingRequest<TemplateVariablesResponse>(
40
+ this,
41
+ {
42
+ method: 'GET',
43
+ url: `mailtemplate/${templateId}/variables`,
44
+ json: true,
45
+ errorContext: 'load template merge codes',
46
+ }
47
+ );
48
+ if (!tagsResponse.success) {
49
+ Logger.error(
50
+ `There was an error with fetching merge codes: ${JSON.stringify(tagsResponse.data)}`
51
+ );
52
+ return { fields: [] };
53
+ }
54
+
55
+ const fields = (tagsResponse?.data.CustomVariables ?? [])
56
+ .map((v) => v?.RawValue)
57
+ .filter(Boolean)
58
+ .map((code) => ({
59
+ id: code,
60
+ displayName: code,
61
+ required: false,
62
+ defaultMatch: false,
63
+ display: true,
64
+ type: 'string' as const,
65
+ canBeUsedToMatch: false,
66
+ }));
67
+
68
+ if (fields.length === 0) return { fields: [noMergeCodesPlaceholder] };
69
+ return { fields };
70
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ ILoadOptionsFunctions,
3
+ INodePropertyOptions,
4
+ LoggerProxy as Logger,
5
+ } from 'n8n-workflow';
6
+ import { MailTemplate } from '../models';
7
+ import { limeMarketingRequest } from '../transport';
8
+
9
+ // loadOptions handler: fetch all mail templates and return them sorted by name
10
+ // for the Template dropdown on the email send operation.
11
+ export async function getTemplates(
12
+ this: ILoadOptionsFunctions
13
+ ): Promise<INodePropertyOptions[]> {
14
+ const templates = await limeMarketingRequest<MailTemplate[]>(this, {
15
+ method: 'GET',
16
+ url: 'mailtemplate',
17
+ json: true,
18
+ errorContext: 'load templates',
19
+ });
20
+ if (!templates.success) {
21
+ Logger.error(
22
+ `There was an error with fetching properties: ${JSON.stringify(templates.data)}`
23
+ );
24
+ return [];
25
+ }
26
+
27
+ return templates.data
28
+ .map((t) => ({
29
+ name: `${t.Name} (ID: ${t.Id})`,
30
+ value: t.Id,
31
+ }))
32
+ .sort((a, b) => a.name.localeCompare(b.name));
33
+ }
@@ -0,0 +1,2 @@
1
+ export { getTemplates } from './getTemplates';
2
+ export { getTemplateMergeCodeMappingColumns } from './getTemplateMergeCodeMappingColumns';
@@ -0,0 +1,12 @@
1
+ export const LIME_MARKETING_API_CREDENTIAL_KEY = 'limeMarketingApi';
2
+ export const TRANSACTIONAL_EMAIL_RESOURCE = 'email';
3
+ export const TRANSACTIONAL_SMS_RESOURCE = 'sms';
4
+
5
+ export const CONTENT_TYPE_TEMPLATE = 'template';
6
+ export const CONTENT_TYPE_CUSTOM = 'html';
7
+ export const CONTENT_TYPE_TEXT = 'text';
8
+
9
+ export const MERGE_CODES_INPUT_FIELDS = 'fields';
10
+ export const MERGE_CODES_INPUT_JSON = 'json';
11
+
12
+ export const SEND_OPERATION = 'send';
@@ -0,0 +1,32 @@
1
+ export {
2
+ LIME_MARKETING_API_CREDENTIAL_KEY,
3
+ TRANSACTIONAL_EMAIL_RESOURCE,
4
+ TRANSACTIONAL_SMS_RESOURCE,
5
+ CONTENT_TYPE_TEMPLATE,
6
+ CONTENT_TYPE_CUSTOM,
7
+ CONTENT_TYPE_TEXT,
8
+ MERGE_CODES_INPUT_FIELDS,
9
+ MERGE_CODES_INPUT_JSON,
10
+ SEND_OPERATION,
11
+ } from './constants';
12
+ export type { MailTemplate } from './template';
13
+ export type {
14
+ AttachmentModel,
15
+ TagModel,
16
+ SendTransactionMailArgs,
17
+ SendTransactionMailBase,
18
+ SendTransactionMailTemplateArgs,
19
+ TransactionMailModel,
20
+ } from './transactionMail';
21
+ export type {
22
+ SendTransactionSmsArgs,
23
+ TransactionSmsModel,
24
+ } from './transactionSms';
25
+ export type {
26
+ TagListInput,
27
+ AttachmentsInput,
28
+ HeadersInput,
29
+ AdditionalMailOptions,
30
+ AdditionalSmsOptions,
31
+ } from './operationInputs';
32
+ export type { LimeMarketingErrorBody } from './transportError';
@@ -0,0 +1,40 @@
1
+ // Named types for the shapes n8n's `getNodeParameter` returns for our
2
+ // fixedCollection / collection inputs. The shapes are dictated by the
3
+ // `properties[]` definitions in each send.operation.ts; lifting them here
4
+ // gives us one source of truth and lets call sites avoid inline `as { ... }`
5
+ // structures.
6
+
7
+ export type TagListInput = {
8
+ tag?: { name: string; value?: string }[];
9
+ };
10
+
11
+ export type AttachmentsInput = {
12
+ attachment?: {
13
+ binaryPropertyName: string;
14
+ fileNameOverride?: string;
15
+ }[];
16
+ };
17
+
18
+ export type HeadersInput = {
19
+ header?: { name: string; value: string }[];
20
+ };
21
+
22
+ export type AdditionalMailOptions = {
23
+ senderName?: string;
24
+ senderEmail?: string;
25
+ replyTo?: string;
26
+ scheduledSendDate?: string;
27
+ externalId?: string;
28
+ excludePublicationOptouts?: boolean;
29
+ excludeTotalOptouts?: boolean;
30
+ excludePreviousBounce?: boolean;
31
+ linkBaseUrl?: string;
32
+ includeEmailDataInWebhookPayload?: boolean;
33
+ headers?: HeadersInput;
34
+ };
35
+
36
+ export type AdditionalSmsOptions = {
37
+ maxParts?: number | null;
38
+ deliveryTimeoutHours?: number | null;
39
+ scheduledSendDate?: string;
40
+ };
@@ -0,0 +1,10 @@
1
+ export type MailTemplate = {
2
+ Id: number;
3
+ HtmlLayoutId: number;
4
+ TextLayoutId: number;
5
+ CreatedByUserId: number;
6
+ Name: string;
7
+ Description: string;
8
+ CreationDate: string;
9
+ ModifiedDate: string;
10
+ };
@@ -0,0 +1,65 @@
1
+ export type AttachmentModel = {
2
+ FileData: string;
3
+ FileNameWithExtension: string;
4
+ MimeType: string;
5
+ };
6
+
7
+ export type TagModel = {
8
+ Name: string;
9
+ Value?: string;
10
+ };
11
+
12
+ export type SendTransactionMailBase = {
13
+ RecipientEmail: string;
14
+ RecipientName?: string;
15
+ SenderName?: string;
16
+ SenderEmail?: string;
17
+ FromName?: string;
18
+ FromEmail?: string;
19
+ ReplyTo?: string;
20
+ Subject?: string;
21
+ ScheduledSendDate?: string;
22
+ ExternalId?: string;
23
+ Attachments?: AttachmentModel[];
24
+ TrackOpenings?: boolean;
25
+ ExcludePublicationOptouts?: boolean;
26
+ ExcludeTotaloptouts?: boolean;
27
+ ExcludePreviousBounce?: boolean;
28
+ TrackLinkClicks?: boolean;
29
+ LinkBaseUrl?: string;
30
+ HtmlContentTagModels?: TagModel[];
31
+ TextContentTagModels?: TagModel[];
32
+ Headers?: Record<string, string>;
33
+ IncludeEmailDataInWebhookPayload?: boolean;
34
+ };
35
+
36
+ export type SendTransactionMailArgs = SendTransactionMailBase & {
37
+ HtmlContent?: number[];
38
+ TextContent?: number[];
39
+ };
40
+
41
+ export type SendTransactionMailTemplateArgs = SendTransactionMailBase & {
42
+ TemplateId: number;
43
+ };
44
+
45
+ export type TransactionMailModel = {
46
+ TransactionMailId: number;
47
+ RecipientName: string;
48
+ RecipientEmail: string;
49
+ SenderName: string;
50
+ SenderEmail: string;
51
+ FromName: string;
52
+ FromEmail: string;
53
+ ReplyTo: string;
54
+ Subject: string;
55
+ TrackOpenings: boolean;
56
+ TrackLinkClicks: boolean;
57
+ LinkBaseUrl: string;
58
+ ExternalId: string;
59
+ ExcludeTotalOptouts: boolean;
60
+ ExcludePublicationOptouts: boolean;
61
+ ExcludePreviousBounce: boolean;
62
+ IsInternalMail: boolean;
63
+ CreationDate: string;
64
+ IncludeEmailDataInWebhookPayload: boolean;
65
+ };