@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
@@ -0,0 +1,228 @@
1
+ import { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
2
+ import {
3
+ TRANSACTIONAL_SMS_RESOURCE,
4
+ SendTransactionSmsArgs,
5
+ TagModel,
6
+ TransactionSmsModel,
7
+ MERGE_CODES_INPUT_FIELDS,
8
+ MERGE_CODES_INPUT_JSON,
9
+ SEND_OPERATION,
10
+ TagListInput,
11
+ AdditionalSmsOptions,
12
+ } from '../../../models';
13
+ import {
14
+ assertValidSmsDestinationNumber,
15
+ assertValidSmsFromNumber,
16
+ omitEmpty,
17
+ } from '../../../utils';
18
+ import {
19
+ mergeCodesInputMethodProperty,
20
+ mergeCodesTagFieldOptions,
21
+ parseMergeCodesJson,
22
+ readFreeFormTagModels,
23
+ } from '../../../utils/mergeCodes';
24
+ import { limeMarketingRequest } from '../../../transport';
25
+
26
+ /** @public */
27
+ export const description = {
28
+ name: 'Send',
29
+ value: SEND_OPERATION,
30
+ description: 'Send an SMS',
31
+ action: 'Send an SMS',
32
+ };
33
+
34
+ /** @public */
35
+ export const properties: INodeProperties[] = [
36
+ {
37
+ displayName: 'From Number',
38
+ name: 'fromNumber',
39
+ type: 'string',
40
+ required: true,
41
+ default: '',
42
+ placeholder: 'e.g. MyCompany',
43
+ description:
44
+ 'The alphanumeric sender ID shown to the recipient (2–11 characters).',
45
+ displayOptions: {
46
+ show: {
47
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
48
+ operation: [SEND_OPERATION],
49
+ },
50
+ },
51
+ },
52
+ {
53
+ displayName: 'Destination Number',
54
+ name: 'destinationNumber',
55
+ type: 'string',
56
+ required: true,
57
+ default: '',
58
+ placeholder: 'e.g. +46701234567',
59
+ description:
60
+ 'The recipient phone number in international format, starting with "+".',
61
+ displayOptions: {
62
+ show: {
63
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
64
+ operation: [SEND_OPERATION],
65
+ },
66
+ },
67
+ },
68
+ {
69
+ displayName: 'Text',
70
+ name: 'text',
71
+ type: 'string',
72
+ typeOptions: { rows: 4 },
73
+ required: true,
74
+ default: '',
75
+ description:
76
+ 'The SMS body. Use merge codes like {{firstname}} and provide values in Merge Code Mapping below.',
77
+ displayOptions: {
78
+ show: {
79
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
80
+ operation: [SEND_OPERATION],
81
+ },
82
+ },
83
+ },
84
+ mergeCodesInputMethodProperty({
85
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
86
+ operation: [SEND_OPERATION],
87
+ }),
88
+ {
89
+ displayName: 'Merge Code Mapping',
90
+ name: 'textContentTagModels',
91
+ type: 'fixedCollection',
92
+ typeOptions: { multipleValues: true },
93
+ placeholder: 'Add Merge Code',
94
+ default: {},
95
+ description: 'Merge codes and values to substitute into the SMS text.',
96
+ displayOptions: {
97
+ show: {
98
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
99
+ operation: [SEND_OPERATION],
100
+ mergeCodesInputMethod: [MERGE_CODES_INPUT_FIELDS],
101
+ },
102
+ },
103
+ options: mergeCodesTagFieldOptions,
104
+ },
105
+ {
106
+ displayName: 'Merge Codes (JSON)',
107
+ name: 'mergeCodesJson',
108
+ type: 'json',
109
+ required: true,
110
+ default: '{\n "{{firstname}}": "John"\n}',
111
+ description:
112
+ 'Object mapping each merge code (with braces) to its replacement value.',
113
+ typeOptions: {
114
+ alwaysOpenEditWindow: true,
115
+ },
116
+ displayOptions: {
117
+ show: {
118
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
119
+ operation: [SEND_OPERATION],
120
+ mergeCodesInputMethod: [MERGE_CODES_INPUT_JSON],
121
+ },
122
+ },
123
+ },
124
+ {
125
+ displayName: 'Additional Options',
126
+ name: 'additionalOptions',
127
+ type: 'collection',
128
+ placeholder: 'Add Option',
129
+ default: {},
130
+ displayOptions: {
131
+ show: {
132
+ resource: [TRANSACTIONAL_SMS_RESOURCE],
133
+ operation: [SEND_OPERATION],
134
+ },
135
+ },
136
+ options: [
137
+ {
138
+ displayName: 'Delivery Timeout Hours',
139
+ name: 'deliveryTimeoutHours',
140
+ type: 'number',
141
+ typeOptions: { minValue: 0, maxValue: 168 },
142
+ default: null,
143
+ description:
144
+ 'How long (in hours, 0–168) the provider should keep retrying delivery before giving up.',
145
+ },
146
+ {
147
+ displayName: 'Max Parts',
148
+ name: 'maxParts',
149
+ type: 'number',
150
+ typeOptions: { minValue: 1 },
151
+ default: null,
152
+ description:
153
+ 'Maximum number of SMS segments the message is allowed to split into.',
154
+ },
155
+ {
156
+ displayName: 'Scheduled Send Date',
157
+ name: 'scheduledSendDate',
158
+ type: 'dateTime',
159
+ default: '',
160
+ description:
161
+ 'When to send the SMS. Leave empty to send immediately.',
162
+ },
163
+ ],
164
+ },
165
+ ];
166
+
167
+ // Read merge codes from whichever UI surface is active (JSON or free-form
168
+ // fixedCollection) and return them as TagModels.
169
+ function readMergeCodes(ctx: IExecuteFunctions, i: number): TagModel[] {
170
+ const method = ctx.getNodeParameter(
171
+ 'mergeCodesInputMethod',
172
+ i,
173
+ MERGE_CODES_INPUT_FIELDS
174
+ ) as string;
175
+ if (method === MERGE_CODES_INPUT_JSON) return parseMergeCodesJson(ctx, i);
176
+ return readFreeFormTagModels(
177
+ ctx.getNodeParameter('textContentTagModels', i, {}) as TagListInput
178
+ );
179
+ }
180
+
181
+ export async function execute(
182
+ this: IExecuteFunctions,
183
+ i: number,
184
+ baseURL?: string
185
+ ) {
186
+ const fromNumber = this.getNodeParameter('fromNumber', i) as string;
187
+ const destinationNumber = this.getNodeParameter(
188
+ 'destinationNumber',
189
+ i
190
+ ) as string;
191
+ const text = this.getNodeParameter('text', i) as string;
192
+
193
+ assertValidSmsFromNumber(this, fromNumber, 'From Number', i);
194
+ assertValidSmsDestinationNumber(
195
+ this,
196
+ destinationNumber,
197
+ 'Destination Number',
198
+ i
199
+ );
200
+
201
+ const additional = this.getNodeParameter(
202
+ 'additionalOptions',
203
+ i,
204
+ {}
205
+ ) as AdditionalSmsOptions;
206
+
207
+ const textContentTagModels = readMergeCodes(this, i);
208
+
209
+ const body: SendTransactionSmsArgs = {
210
+ Text: text,
211
+ FromNumber: fromNumber,
212
+ DestinationNumber: destinationNumber,
213
+ MaxParts: omitEmpty(additional.maxParts),
214
+ DeliveryTimeoutHours: omitEmpty(additional.deliveryTimeoutHours),
215
+ ScheduledSendDate: omitEmpty(additional.scheduledSendDate),
216
+ TextContentTagModels:
217
+ textContentTagModels.length > 0 ? textContentTagModels : undefined,
218
+ };
219
+
220
+ return limeMarketingRequest<TransactionSmsModel>(this, {
221
+ method: 'POST',
222
+ url: 'transactionsms/send',
223
+ body,
224
+ json: true,
225
+ errorContext: 'send SMS',
226
+ baseURL,
227
+ });
228
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ IAllExecuteFunctions,
3
+ IHttpRequestOptions,
4
+ NodeApiError,
5
+ } from 'n8n-workflow';
6
+ import { LIME_MARKETING_API_CREDENTIAL_KEY } from '../models';
7
+ import { getBaseUrl } from '../utils';
8
+ import { handleWorkflowError } from '../../errorHandling';
9
+ import { APIResponse } from '../../response';
10
+ import { LimeMarketingErrorBody } from '../models';
11
+
12
+ const formatErrorDetails = (
13
+ details: LimeMarketingErrorBody['ErrorDetails']
14
+ ): string =>
15
+ (details ?? [])
16
+ .map((d) => [d?.Message, d?.Context].filter(Boolean).join(': '))
17
+ .filter(Boolean)
18
+ .join('; ');
19
+
20
+ const createErrorMessage = (error: NodeApiError): string => {
21
+ const data = error.context.data as LimeMarketingErrorBody;
22
+ const message = data.Message ?? '';
23
+ const details = formatErrorDetails(data.ErrorDetails);
24
+ return details ? `${message}: ${details}` : message;
25
+ };
26
+
27
+ export type LimeMarketingRequestOptions = Omit<
28
+ IHttpRequestOptions,
29
+ 'baseURL'
30
+ > & {
31
+ /**
32
+ * Action being performed, used to prefix error messages.
33
+ * Example: 'send email' → "Failed to send email: <api message>".
34
+ */
35
+ errorContext?: string;
36
+ /**
37
+ * Pre-resolved base URL. Pass it once at the top of execute() to avoid
38
+ * re-fetching credentials on every per-item HTTP call.
39
+ */
40
+ baseURL?: string;
41
+ };
42
+
43
+ export async function limeMarketingRequest<T>(
44
+ context: IAllExecuteFunctions,
45
+ options: LimeMarketingRequestOptions
46
+ ): Promise<APIResponse<T>> {
47
+ const { baseURL: providedBaseURL, ...httpOptions } = options;
48
+ const baseURL = providedBaseURL ?? (await getBaseUrl(context));
49
+ try {
50
+ const data = await context.helpers.httpRequestWithAuthentication.call(
51
+ context,
52
+ LIME_MARKETING_API_CREDENTIAL_KEY,
53
+ { ...httpOptions, baseURL }
54
+ );
55
+ return {
56
+ success: true,
57
+ data: data,
58
+ };
59
+ } catch (error) {
60
+ return handleWorkflowError(
61
+ context.getNode(),
62
+ {
63
+ message: createErrorMessage(error),
64
+ },
65
+ true
66
+ );
67
+ }
68
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ IAllExecuteFunctions,
3
+ IExecuteFunctions,
4
+ NodeOperationError,
5
+ } from 'n8n-workflow';
6
+ import { LIME_MARKETING_API_CREDENTIAL_KEY, AttachmentModel } from '../models';
7
+
8
+ /**
9
+ * Matches the server-side Lime Marketing email regex so we fail fast in the
10
+ * node rather than round-tripping an obviously bad address to the API.
11
+ *
12
+ * Source:
13
+ * EmailUsernamePartRegex = @"([a-zA-Z0-9_\-\.'\+\!\#\$\%\&\?\=\^\{\}\|\~\*]+)"
14
+ * EmailRegexPattern = "^" + username + "@((\[ipv4\])|(([a-zA-Z0-9-_]+\.)*([a-zA-Z0-9-]+)\.[a-zA-Z0-9]{2,15}))$"
15
+ */
16
+ const LIME_MARKETING_EMAIL_REGEX =
17
+ // eslint-disable-next-line sonarjs/regex-complexity, sonarjs/concise-regex
18
+ /^([a-zA-Z0-9_\-.'+!#$%&?=^{}|~*]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z0-9\-_]+\.)*([a-zA-Z0-9-]+)\.[a-zA-Z0-9]{2,15}))$/;
19
+
20
+ export function assertValidEmail(
21
+ context: IExecuteFunctions,
22
+ email: string,
23
+ fieldLabel: string,
24
+ itemIndex: number
25
+ ): void {
26
+ if (LIME_MARKETING_EMAIL_REGEX.test(email)) return;
27
+ throw new NodeOperationError(
28
+ context.getNode(),
29
+ `${fieldLabel} is not a valid email address: ${email}`,
30
+ {
31
+ itemIndex,
32
+ description:
33
+ 'Expected a format like name@example.com. Lime Marketing rejects addresses that do not match its email pattern.',
34
+ }
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Destination phone number regex from the /transactionsms/send schema:
40
+ * ^([+]+[1-9]{2})+\d{5,20}$
41
+ * Expects an E.164-style number starting with "+" and a non-zero country code.
42
+ */
43
+ const LIME_MARKETING_SMS_DESTINATION_REGEX = /^([+]+[1-9]{2})+\d{5,20}$/;
44
+
45
+ /**
46
+ * Sender (FromNumber) regex from the /transactionsms/send schema — alphanumeric
47
+ * sender ID, 2–11 chars, Nordic letters + digits + _ - allowed, spaces allowed
48
+ * but not as first/last character:
49
+ * ^[a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d][a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d\s]{0,9}[a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d]$
50
+ */
51
+ const LIME_MARKETING_SMS_FROM_NUMBER_REGEX =
52
+ /^[a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d][a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d\s]{0,9}[a-zA-ZåäöÅÄÖæÆøØüÜ_\-\d]$/;
53
+
54
+ export function assertValidSmsDestinationNumber(
55
+ context: IExecuteFunctions,
56
+ value: string,
57
+ fieldLabel: string,
58
+ itemIndex: number
59
+ ): void {
60
+ if (LIME_MARKETING_SMS_DESTINATION_REGEX.test(value)) return;
61
+ throw new NodeOperationError(
62
+ context.getNode(),
63
+ `${fieldLabel} is not a valid phone number: ${value}`,
64
+ {
65
+ itemIndex,
66
+ description:
67
+ 'Expected an international format starting with "+", e.g. +46701234567.',
68
+ }
69
+ );
70
+ }
71
+
72
+ export function assertValidSmsFromNumber(
73
+ context: IExecuteFunctions,
74
+ value: string,
75
+ fieldLabel: string,
76
+ itemIndex: number
77
+ ): void {
78
+ if (LIME_MARKETING_SMS_FROM_NUMBER_REGEX.test(value)) return;
79
+ throw new NodeOperationError(
80
+ context.getNode(),
81
+ `${fieldLabel} is not a valid sender ID: ${value}`,
82
+ {
83
+ itemIndex,
84
+ description:
85
+ 'Expected 2–11 characters: letters, digits, underscores, hyphens, or spaces (spaces not allowed at start/end).',
86
+ }
87
+ );
88
+ }
89
+
90
+ type LimeMarketingCredentials = {
91
+ url: string;
92
+ apiKey: string;
93
+ };
94
+
95
+ export async function getBaseUrl(
96
+ context: IAllExecuteFunctions
97
+ ): Promise<string> {
98
+ const credentials = (await context.getCredentials(
99
+ LIME_MARKETING_API_CREDENTIAL_KEY
100
+ )) as unknown as LimeMarketingCredentials;
101
+ let url = credentials.url;
102
+ while (url.endsWith('/')) url = url.slice(0, -1);
103
+ return url;
104
+ }
105
+
106
+ /**
107
+ * Coerces empty/null values to undefined so the property is dropped from the
108
+ * outbound JSON payload rather than sent as "" or null.
109
+ *
110
+ * n8n's `collection` UI defaults to '' for string fields and null for number
111
+ * fields with `default: null`; we treat both as "user did not set this".
112
+ *
113
+ * @param value - The value to normalize.
114
+ */
115
+ export function omitEmpty<T>(value: T | '' | null | undefined): T | undefined {
116
+ if (value === '' || value === null || value === undefined) return undefined;
117
+ return value;
118
+ }
119
+
120
+ export async function buildAttachment(
121
+ context: IExecuteFunctions,
122
+ i: number,
123
+ binaryPropertyName: string,
124
+ fileNameOverride?: string
125
+ ): Promise<AttachmentModel> {
126
+ const binaryData = context.helpers.assertBinaryData(i, binaryPropertyName);
127
+ const fileName = fileNameOverride || binaryData.fileName;
128
+ if (!fileName) {
129
+ throw new NodeOperationError(
130
+ context.getNode(),
131
+ `Attachment "${binaryPropertyName}" is missing a file name. Set one via "File Name Override" or ensure the upstream node sets it.`
132
+ );
133
+ }
134
+ return {
135
+ FileData: binaryData.data,
136
+ FileNameWithExtension: fileName,
137
+ MimeType: binaryData.mimeType,
138
+ };
139
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ IDisplayOptions,
3
+ IExecuteFunctions,
4
+ INodeProperties,
5
+ NodeOperationError,
6
+ } from 'n8n-workflow';
7
+ import {
8
+ MERGE_CODES_INPUT_FIELDS,
9
+ MERGE_CODES_INPUT_JSON,
10
+ TagListInput,
11
+ TagModel,
12
+ } from '../models';
13
+
14
+ // Read the "Merge Codes (JSON)" parameter (already-parsed object or string)
15
+ // and turn it into TagModels. Throws NodeOperationError with item context on
16
+ // invalid JSON or non-object roots.
17
+ export function parseMergeCodesJson(
18
+ ctx: IExecuteFunctions,
19
+ i: number
20
+ ): TagModel[] {
21
+ const jsonInput = ctx.getNodeParameter('mergeCodesJson', i) as
22
+ | string
23
+ | Record<string, string>;
24
+ let parsed: Record<string, unknown>;
25
+ try {
26
+ parsed =
27
+ typeof jsonInput === 'string' ? JSON.parse(jsonInput) : jsonInput;
28
+ } catch (error_) {
29
+ const message =
30
+ error_ instanceof Error ? error_.message : String(error_);
31
+ throw new NodeOperationError(
32
+ ctx.getNode(),
33
+ `Merge Codes (JSON) is not valid JSON: ${message}`,
34
+ {
35
+ itemIndex: i,
36
+ description:
37
+ 'Enter a valid JSON object, for example: { "{{firstname}}": "John" }',
38
+ }
39
+ );
40
+ }
41
+ if (
42
+ parsed === null ||
43
+ typeof parsed !== 'object' ||
44
+ Array.isArray(parsed)
45
+ ) {
46
+ throw new NodeOperationError(
47
+ ctx.getNode(),
48
+ 'Merge Codes (JSON) must be a JSON object of { mergeCode: value } pairs.',
49
+ {
50
+ itemIndex: i,
51
+ description:
52
+ 'Example: { "{{firstname}}": "John", "{{custom}}": "value" }',
53
+ }
54
+ );
55
+ }
56
+ // Match the template-mapper path: send "" rather than the literal
57
+ // strings "null" / "undefined" when the user supplied either.
58
+ return Object.entries(parsed).map(([Name, Value]) => ({
59
+ Name,
60
+ Value: Value === null || Value === undefined ? '' : String(Value),
61
+ }));
62
+ }
63
+
64
+ export function readFreeFormTagModels(
65
+ input: TagListInput | undefined
66
+ ): TagModel[] {
67
+ return (input?.tag ?? []).map((t) => ({ Name: t.name, Value: t.value }));
68
+ }
69
+
70
+ // Build the "Merge Codes Input Method" UI dropdown (Form vs JSON Object) for
71
+ // the given displayOptions.show selector. Reused by both email and SMS sends.
72
+ export function mergeCodesInputMethodProperty(
73
+ displayOptionsShow: NonNullable<IDisplayOptions['show']>
74
+ ): INodeProperties {
75
+ return {
76
+ displayName: 'Merge Codes Input Method',
77
+ name: 'mergeCodesInputMethod',
78
+ type: 'options',
79
+ options: [
80
+ {
81
+ name: 'Form',
82
+ value: MERGE_CODES_INPUT_FIELDS,
83
+ description: 'Provide merge codes and values using the UI',
84
+ },
85
+ {
86
+ name: 'JSON Object',
87
+ value: MERGE_CODES_INPUT_JSON,
88
+ description: 'Provide merge codes and values as a JSON object',
89
+ },
90
+ ],
91
+ default: MERGE_CODES_INPUT_FIELDS,
92
+ description:
93
+ 'Form: fill in merge codes and values. JSON: provide them as an object.',
94
+ displayOptions: { show: displayOptionsShow },
95
+ };
96
+ }
97
+
98
+ export const mergeCodesTagFieldOptions = [
99
+ {
100
+ name: 'tag',
101
+ displayName: 'Merge Code',
102
+ values: [
103
+ {
104
+ displayName: 'Name',
105
+ name: 'name',
106
+ type: 'string' as const,
107
+ default: '',
108
+ required: true,
109
+ placeholder: 'e.g. {{firstname}}',
110
+ description:
111
+ 'The full merge code including braces, e.g. {{firstname}}.',
112
+ },
113
+ {
114
+ displayName: 'Value',
115
+ name: 'value',
116
+ type: 'string' as const,
117
+ default: '',
118
+ },
119
+ ],
120
+ },
121
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limetech/n8n-nodes-lime",
3
- "version": "3.11.0-dev.1",
3
+ "version": "3.11.0",
4
4
  "description": "n8n node to connect to Lime CRM",
5
5
  "author": {
6
6
  "name": "Lime Technologies",
@@ -9,7 +9,7 @@
9
9
  "license": "Apache-2.0",
10
10
  "main": "nodes/index.ts",
11
11
  "scripts": {
12
- "build": "tsc -p tsconfig.build.json && copyfiles \"nodes/**/*.svg\" dist",
12
+ "build": "tsc -p tsconfig.build.json && copyfiles \"nodes/**/*.svg\" \"credentials/**/*.svg\" dist",
13
13
  "dev": "tsc --watch",
14
14
  "watch": "tsc --watch",
15
15
  "lint": "eslint --max-warnings=0 .",
@@ -34,12 +34,14 @@
34
34
  "n8nNodesApiVersion": 1,
35
35
  "credentials": [
36
36
  "dist/credentials/LimeCrmApi.credentials.js",
37
- "dist/credentials/LimeFormsApi.credentials.js"
37
+ "dist/credentials/LimeFormsApi.credentials.js",
38
+ "dist/credentials/LimeMarketingApi.credentials.js"
38
39
  ],
39
40
  "nodes": [
40
41
  "dist/nodes/lime-crm/LimeCrmNode.node.js",
41
42
  "dist/nodes/lime-crm/LimeCrmTrigger.node.js",
42
- "dist/nodes/lime-forms/LimeFormsTrigger.node.js"
43
+ "dist/nodes/lime-forms/LimeFormsTrigger.node.js",
44
+ "dist/nodes/lime-marketing/LimeMarketing.node.js"
43
45
  ]
44
46
  },
45
47
  "devDependencies": {
@@ -67,8 +69,5 @@
67
69
  },
68
70
  "publishConfig": {
69
71
  "access": "public"
70
- },
71
- "dependencies": {
72
- "csv-parse": "^7.0.0"
73
72
  }
74
73
  }