n8n-nodes-lemonsqueezy 0.5.0 → 0.7.1
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/README.md +43 -2
- package/dist/nodes/LemonSqueezy/LemonSqueezy.node.js +35 -1
- package/dist/nodes/LemonSqueezy/LemonSqueezyTrigger.node.js +46 -22
- package/dist/nodes/LemonSqueezy/constants.d.ts +2 -0
- package/dist/nodes/LemonSqueezy/constants.js +3 -1
- package/dist/nodes/LemonSqueezy/helpers.d.ts +7 -2
- package/dist/nodes/LemonSqueezy/helpers.js +17 -10
- package/dist/nodes/LemonSqueezy/resources/shared.d.ts +35 -0
- package/dist/nodes/LemonSqueezy/resources/shared.js +94 -1
- package/dist/nodes/LemonSqueezy/resources/usageRecord.js +63 -0
- package/dist/nodes/LemonSqueezy/resources/webhook.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ The main node for interacting with the Lemon Squeezy API.
|
|
|
65
65
|
| **Store** | Get, Get Many |
|
|
66
66
|
| **Subscription** | Get, Get Many, Update, Cancel, Resume |
|
|
67
67
|
| **Subscription Invoice** | Get, Get Many |
|
|
68
|
-
| **Usage Record** | Get, Get Many |
|
|
68
|
+
| **Usage Record** | Create, Get, Get Many |
|
|
69
69
|
| **User** | Get Current |
|
|
70
70
|
| **Variant** | Get, Get Many |
|
|
71
71
|
| **Webhook** | Create, Update, Delete, Get, Get Many |
|
|
@@ -311,6 +311,47 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
311
311
|
|
|
312
312
|
## Changelog
|
|
313
313
|
|
|
314
|
+
### v0.7.1
|
|
315
|
+
|
|
316
|
+
**n8n Community Package Compliance:**
|
|
317
|
+
- Resolved all n8n community package scanner ESLint violations
|
|
318
|
+
- Replaced deprecated `requestWithAuthentication` with `httpRequestWithAuthentication`
|
|
319
|
+
- Fixed restricted `setTimeout` global usage
|
|
320
|
+
|
|
321
|
+
### v0.7.0
|
|
322
|
+
|
|
323
|
+
**New Features:**
|
|
324
|
+
- Added Usage Record Create operation for metered billing support
|
|
325
|
+
- Added configurable pagination timeout in Advanced Options UI for "Return All" operations
|
|
326
|
+
- Added field hints with examples and documentation links for better UX
|
|
327
|
+
- Added CHANGELOG.md with migration guide for breaking changes
|
|
328
|
+
|
|
329
|
+
**Security:**
|
|
330
|
+
- Increased webhook secret minimum length from 16 to 32 characters
|
|
331
|
+
- Added webhook creation deduplication to prevent race conditions
|
|
332
|
+
|
|
333
|
+
**Bug Fixes:**
|
|
334
|
+
- Fixed pagination timeout=0 handling (now correctly treated as "no timeout")
|
|
335
|
+
|
|
336
|
+
### v0.6.0
|
|
337
|
+
|
|
338
|
+
**Reliability & Error Handling:**
|
|
339
|
+
- Improved webhook management error handling with proper 404 vs other error distinction
|
|
340
|
+
|
|
341
|
+
**Input Validation:**
|
|
342
|
+
- Added pre-API validation for email fields (customer create/update, checkout)
|
|
343
|
+
- Added pre-API validation for URL fields (webhook URL, redirect URLs, receipt link URLs)
|
|
344
|
+
- Added webhook secret minimum length validation (16 characters) for security
|
|
345
|
+
- Validation errors now fail fast before making API requests
|
|
346
|
+
|
|
347
|
+
**Performance:**
|
|
348
|
+
- Added configurable request timeout (default: 30 seconds) for all API requests
|
|
349
|
+
- Timeout prevents hanging requests and improves workflow reliability
|
|
350
|
+
|
|
351
|
+
**Code Quality:**
|
|
352
|
+
- Added common filter field generators to reduce code duplication
|
|
353
|
+
- Added createFiltersField, createStatusFilter factory functions
|
|
354
|
+
|
|
314
355
|
### v0.5.0
|
|
315
356
|
|
|
316
357
|
**Security & Stability Improvements:**
|
|
@@ -319,7 +360,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
319
360
|
- Improved email validation using RFC 5322 compliant regex
|
|
320
361
|
- Enhanced URL validation to block internal/private network URLs (SSRF protection)
|
|
321
362
|
- IPv6 localhost blocking (`[::1]`) for complete SSRF protection
|
|
322
|
-
-
|
|
363
|
+
- Improved error handling with proper error propagation
|
|
323
364
|
- Added proper null checks and type safety for custom data handling
|
|
324
365
|
|
|
325
366
|
**New Features:**
|
|
@@ -10,6 +10,8 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
10
10
|
const name = ctx.getNodeParameter('customerName', itemIndex);
|
|
11
11
|
const email = ctx.getNodeParameter('customerEmail', itemIndex);
|
|
12
12
|
const additionalFields = ctx.getNodeParameter('additionalFields', itemIndex);
|
|
13
|
+
// Validate required fields before API call
|
|
14
|
+
(0, helpers_1.validateField)('email', email, 'email');
|
|
13
15
|
const body = (0, helpers_1.buildJsonApiBody)('customers', { name, email, ...additionalFields }, { store: { type: 'stores', id: storeId } });
|
|
14
16
|
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/customers', body);
|
|
15
17
|
}
|
|
@@ -63,6 +65,8 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
63
65
|
attributes.custom_price = additionalOptions.customPrice;
|
|
64
66
|
}
|
|
65
67
|
if (additionalOptions.email) {
|
|
68
|
+
// Validate email before API call
|
|
69
|
+
(0, helpers_1.validateField)('email', additionalOptions.email, 'email');
|
|
66
70
|
checkoutData.email = additionalOptions.email;
|
|
67
71
|
}
|
|
68
72
|
if (additionalOptions.name) {
|
|
@@ -72,12 +76,16 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
72
76
|
checkoutData.discount_code = additionalOptions.discountCode;
|
|
73
77
|
}
|
|
74
78
|
if (additionalOptions.redirectUrl) {
|
|
79
|
+
// Validate URL before API call
|
|
80
|
+
(0, helpers_1.validateField)('redirectUrl', additionalOptions.redirectUrl, 'url');
|
|
75
81
|
productOptions.redirect_url = additionalOptions.redirectUrl;
|
|
76
82
|
}
|
|
77
83
|
if (additionalOptions.receiptButtonText) {
|
|
78
84
|
productOptions.receipt_button_text = additionalOptions.receiptButtonText;
|
|
79
85
|
}
|
|
80
86
|
if (additionalOptions.receiptLinkUrl) {
|
|
87
|
+
// Validate URL before API call
|
|
88
|
+
(0, helpers_1.validateField)('receiptLinkUrl', additionalOptions.receiptLinkUrl, 'url');
|
|
81
89
|
productOptions.receipt_link_url = additionalOptions.receiptLinkUrl;
|
|
82
90
|
}
|
|
83
91
|
if (additionalOptions.receiptThankYouNote) {
|
|
@@ -148,12 +156,25 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
148
156
|
});
|
|
149
157
|
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/checkouts', body);
|
|
150
158
|
}
|
|
159
|
+
if (resource === 'usageRecord') {
|
|
160
|
+
const subscriptionItemId = ctx.getNodeParameter('subscriptionItemId', itemIndex);
|
|
161
|
+
const quantity = ctx.getNodeParameter('quantity', itemIndex);
|
|
162
|
+
const action = ctx.getNodeParameter('action', itemIndex);
|
|
163
|
+
const body = (0, helpers_1.buildJsonApiBody)('usage-records', { quantity, action }, { 'subscription-item': { type: 'subscription-items', id: subscriptionItemId } });
|
|
164
|
+
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/usage-records', body);
|
|
165
|
+
}
|
|
151
166
|
if (resource === 'webhook') {
|
|
152
167
|
const storeId = ctx.getNodeParameter('webhookStoreId', itemIndex);
|
|
153
168
|
const url = ctx.getNodeParameter('webhookUrl', itemIndex);
|
|
154
169
|
const events = ctx.getNodeParameter('webhookEvents', itemIndex);
|
|
155
170
|
const secret = ctx.getNodeParameter('webhookSecret', itemIndex);
|
|
156
171
|
const additionalOptions = ctx.getNodeParameter('additionalOptions', itemIndex, {});
|
|
172
|
+
// Validate URL before API call
|
|
173
|
+
(0, helpers_1.validateField)('url', url, 'url');
|
|
174
|
+
// Validate webhook secret minimum length for security (32+ chars recommended)
|
|
175
|
+
if (secret.length < 32) {
|
|
176
|
+
throw new Error('Webhook secret must be at least 32 characters for security. Generate one using: openssl rand -hex 32');
|
|
177
|
+
}
|
|
157
178
|
const attributes = { url, events, secret };
|
|
158
179
|
if (additionalOptions.testMode !== undefined) {
|
|
159
180
|
attributes.test_mode = additionalOptions.testMode;
|
|
@@ -202,6 +223,8 @@ async function handleUpdate(ctx, resource, itemIndex) {
|
|
|
202
223
|
attributes.name = updateFields.name;
|
|
203
224
|
}
|
|
204
225
|
if (updateFields.email) {
|
|
226
|
+
// Validate email before API call
|
|
227
|
+
(0, helpers_1.validateField)('email', updateFields.email, 'email');
|
|
205
228
|
attributes.email = updateFields.email;
|
|
206
229
|
}
|
|
207
230
|
if (updateFields.city) {
|
|
@@ -240,12 +263,18 @@ async function handleUpdate(ctx, resource, itemIndex) {
|
|
|
240
263
|
const updateFields = ctx.getNodeParameter('updateFields', itemIndex);
|
|
241
264
|
const attributes = {};
|
|
242
265
|
if (updateFields.url) {
|
|
266
|
+
// Validate URL before API call
|
|
267
|
+
(0, helpers_1.validateField)('url', updateFields.url, 'url');
|
|
243
268
|
attributes.url = updateFields.url;
|
|
244
269
|
}
|
|
245
270
|
if (updateFields.events) {
|
|
246
271
|
attributes.events = updateFields.events;
|
|
247
272
|
}
|
|
248
273
|
if (updateFields.secret) {
|
|
274
|
+
// Validate webhook secret minimum length for security (32+ chars recommended)
|
|
275
|
+
if (updateFields.secret.length < 32) {
|
|
276
|
+
throw new Error('Webhook secret must be at least 32 characters for security. Generate one using: openssl rand -hex 32');
|
|
277
|
+
}
|
|
249
278
|
attributes.secret = updateFields.secret;
|
|
250
279
|
}
|
|
251
280
|
const body = (0, helpers_1.buildJsonApiBody)('webhooks', attributes, undefined, webhookId);
|
|
@@ -278,6 +307,7 @@ class LemonSqueezy {
|
|
|
278
307
|
};
|
|
279
308
|
}
|
|
280
309
|
async execute() {
|
|
310
|
+
var _a;
|
|
281
311
|
const items = this.getInputData();
|
|
282
312
|
const returnData = [];
|
|
283
313
|
const resource = this.getNodeParameter('resource', 0);
|
|
@@ -310,7 +340,11 @@ class LemonSqueezy {
|
|
|
310
340
|
}
|
|
311
341
|
}
|
|
312
342
|
if (returnAll) {
|
|
313
|
-
|
|
343
|
+
// Convert pagination timeout from seconds to milliseconds (0 = no timeout)
|
|
344
|
+
const paginationTimeout = (_a = advancedOptions.paginationTimeout) !== null && _a !== void 0 ? _a : 300;
|
|
345
|
+
responseData = await helpers_1.lemonSqueezyApiRequestAllItems.call(this, 'GET', `/${endpoint}`, qs, {
|
|
346
|
+
timeout: paginationTimeout > 0 ? paginationTimeout * 1000 : 0,
|
|
347
|
+
});
|
|
314
348
|
}
|
|
315
349
|
else {
|
|
316
350
|
const limit = this.getNodeParameter('limit', i);
|
|
@@ -98,10 +98,9 @@ class LemonSqueezyTrigger {
|
|
|
98
98
|
return true;
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
101
|
-
// Webhook
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
console.debug(`Webhook ${String(webhookData.webhookId)} check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
101
|
+
// Webhook not found or error occurred - will recreate
|
|
102
|
+
// Silently handle both 404 (deleted externally) and other errors
|
|
103
|
+
void error; // Acknowledge error without logging
|
|
105
104
|
delete webhookData.webhookId;
|
|
106
105
|
return false;
|
|
107
106
|
}
|
|
@@ -121,11 +120,8 @@ class LemonSqueezyTrigger {
|
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
catch
|
|
125
|
-
// Error checking webhooks -
|
|
126
|
-
// This could indicate API issues, but we'll try to create a new webhook
|
|
127
|
-
// eslint-disable-next-line no-console
|
|
128
|
-
console.debug(`Error checking existing webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
123
|
+
catch {
|
|
124
|
+
// Error checking webhooks - will attempt to create new webhook
|
|
129
125
|
}
|
|
130
126
|
return false;
|
|
131
127
|
},
|
|
@@ -155,26 +151,54 @@ class LemonSqueezyTrigger {
|
|
|
155
151
|
},
|
|
156
152
|
},
|
|
157
153
|
};
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
try {
|
|
155
|
+
const response = await helpers_1.lemonSqueezyApiRequest.call(this, 'POST', '/webhooks', body);
|
|
156
|
+
const responseData = response;
|
|
157
|
+
const data = responseData.data;
|
|
158
|
+
if (data === null || data === void 0 ? void 0 : data.id) {
|
|
159
|
+
webhookData.webhookId = data.id;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Handle race condition: if webhook with same URL was created between checkExists and create
|
|
165
|
+
// Check if error is 409 Conflict or similar, then try to find existing webhook
|
|
166
|
+
const isConflictOrDuplicate = error &&
|
|
167
|
+
typeof error === 'object' &&
|
|
168
|
+
(error.statusCode === 409 ||
|
|
169
|
+
error.statusCode === 422);
|
|
170
|
+
if (isConflictOrDuplicate) {
|
|
171
|
+
// Try to find the existing webhook
|
|
172
|
+
try {
|
|
173
|
+
const existingResponse = await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', '/webhooks', undefined, { 'filter[store_id]': storeId });
|
|
174
|
+
const existingData = existingResponse;
|
|
175
|
+
const webhooks = existingData.data;
|
|
176
|
+
if (Array.isArray(webhooks)) {
|
|
177
|
+
const existingWebhook = webhooks.find((webhook) => { var _a; return ((_a = webhook.attributes) === null || _a === void 0 ? void 0 : _a.url) === webhookUrl; });
|
|
178
|
+
if (existingWebhook === null || existingWebhook === void 0 ? void 0 : existingWebhook.id) {
|
|
179
|
+
webhookData.webhookId = existingWebhook.id;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Failed to fetch existing webhook after conflict - will re-throw original error
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Re-throw original error if not a handled conflict
|
|
189
|
+
throw error;
|
|
164
190
|
}
|
|
165
191
|
return false;
|
|
166
192
|
},
|
|
167
193
|
async delete() {
|
|
168
194
|
const webhookData = this.getWorkflowStaticData('node');
|
|
169
195
|
if (webhookData.webhookId) {
|
|
196
|
+
const webhookId = String(webhookData.webhookId);
|
|
170
197
|
try {
|
|
171
|
-
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${
|
|
198
|
+
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${webhookId}`);
|
|
172
199
|
}
|
|
173
|
-
catch
|
|
174
|
-
//
|
|
175
|
-
// Log for debugging but don't fail - we're cleaning up anyway
|
|
176
|
-
// eslint-disable-next-line no-console
|
|
177
|
-
console.debug(`Webhook ${String(webhookData.webhookId)} deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
200
|
+
catch {
|
|
201
|
+
// Silently handle deletion errors (404 = already deleted, others = continue cleanup)
|
|
178
202
|
}
|
|
179
203
|
delete webhookData.webhookId;
|
|
180
204
|
}
|
|
@@ -255,7 +279,7 @@ class LemonSqueezyTrigger {
|
|
|
255
279
|
return {
|
|
256
280
|
webhookResponse: {
|
|
257
281
|
status: 200,
|
|
258
|
-
body: { received: true, processed: false },
|
|
282
|
+
body: { received: true, processed: false, event: eventName },
|
|
259
283
|
},
|
|
260
284
|
};
|
|
261
285
|
}
|
|
@@ -6,6 +6,8 @@ export declare const DEFAULT_PAGE_SIZE = 100;
|
|
|
6
6
|
export declare const MAX_RETRIES = 3;
|
|
7
7
|
export declare const RETRY_DELAY_MS = 1000;
|
|
8
8
|
export declare const RATE_LIMIT_DELAY_MS = 60000;
|
|
9
|
+
/** Default timeout for API requests in milliseconds (30 seconds) */
|
|
10
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
9
11
|
/**
|
|
10
12
|
* Resource to API endpoint mapping
|
|
11
13
|
*/
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* Lemon Squeezy API constants
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.INTERVAL_TYPES = exports.PRODUCT_STATUSES = exports.PAUSE_MODES = exports.DISCOUNT_DURATION_TYPES = exports.DISCOUNT_AMOUNT_TYPES = exports.LICENSE_KEY_STATUSES = exports.CUSTOMER_STATUSES = exports.ORDER_STATUSES = exports.SUBSCRIPTION_STATUSES = exports.WEBHOOK_EVENTS = exports.RESOURCE_ID_PARAMS = exports.RESOURCE_ENDPOINTS = exports.RATE_LIMIT_DELAY_MS = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.DEFAULT_PAGE_SIZE = exports.API_BASE_URL = void 0;
|
|
6
|
+
exports.INTERVAL_TYPES = exports.PRODUCT_STATUSES = exports.PAUSE_MODES = exports.DISCOUNT_DURATION_TYPES = exports.DISCOUNT_AMOUNT_TYPES = exports.LICENSE_KEY_STATUSES = exports.CUSTOMER_STATUSES = exports.ORDER_STATUSES = exports.SUBSCRIPTION_STATUSES = exports.WEBHOOK_EVENTS = exports.RESOURCE_ID_PARAMS = exports.RESOURCE_ENDPOINTS = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.RATE_LIMIT_DELAY_MS = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.DEFAULT_PAGE_SIZE = exports.API_BASE_URL = void 0;
|
|
7
7
|
exports.API_BASE_URL = 'https://api.lemonsqueezy.com/v1';
|
|
8
8
|
exports.DEFAULT_PAGE_SIZE = 100;
|
|
9
9
|
exports.MAX_RETRIES = 3;
|
|
10
10
|
exports.RETRY_DELAY_MS = 1000;
|
|
11
11
|
exports.RATE_LIMIT_DELAY_MS = 60000;
|
|
12
|
+
/** Default timeout for API requests in milliseconds (30 seconds) */
|
|
13
|
+
exports.DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
12
14
|
/**
|
|
13
15
|
* Resource to API endpoint mapping
|
|
14
16
|
*/
|
|
@@ -167,6 +167,7 @@ export declare function isRetryableError(error: unknown): boolean;
|
|
|
167
167
|
* - Automatic authentication using stored credentials
|
|
168
168
|
* - Rate limit handling with automatic retry after delay
|
|
169
169
|
* - Exponential backoff for server errors (5xx)
|
|
170
|
+
* - Configurable request timeout (default: 30 seconds)
|
|
170
171
|
* - Detailed error messages using NodeApiError
|
|
171
172
|
*
|
|
172
173
|
* @param this - The n8n execution context
|
|
@@ -174,8 +175,9 @@ export declare function isRetryableError(error: unknown): boolean;
|
|
|
174
175
|
* @param endpoint - API endpoint path (e.g., '/v1/products')
|
|
175
176
|
* @param body - Optional request body for POST/PATCH requests
|
|
176
177
|
* @param qs - Optional query string parameters
|
|
178
|
+
* @param timeout - Request timeout in milliseconds (default: 30000)
|
|
177
179
|
* @returns The API response data
|
|
178
|
-
* @throws NodeApiError if the request fails after all retries
|
|
180
|
+
* @throws NodeApiError if the request fails after all retries or times out
|
|
179
181
|
*
|
|
180
182
|
* @example
|
|
181
183
|
* // GET request
|
|
@@ -185,8 +187,11 @@ export declare function isRetryableError(error: unknown): boolean;
|
|
|
185
187
|
* const checkout = await lemonSqueezyApiRequest.call(this, 'POST', '/v1/checkouts', {
|
|
186
188
|
* data: { type: 'checkouts', attributes: { ... } }
|
|
187
189
|
* })
|
|
190
|
+
*
|
|
191
|
+
* // Request with custom timeout (60 seconds)
|
|
192
|
+
* const data = await lemonSqueezyApiRequest.call(this, 'GET', '/v1/orders', undefined, {}, 60000)
|
|
188
193
|
*/
|
|
189
|
-
export declare function lemonSqueezyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions, method: IHttpRequestMethods, endpoint: string, body?: IDataObject, qs?: Record<string, string | number
|
|
194
|
+
export declare function lemonSqueezyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions, method: IHttpRequestMethods, endpoint: string, body?: IDataObject, qs?: Record<string, string | number>, timeout?: number): Promise<IDataObject>;
|
|
190
195
|
/**
|
|
191
196
|
* Makes paginated requests to fetch all items from a Lemon Squeezy API endpoint.
|
|
192
197
|
*
|
|
@@ -270,6 +270,8 @@ function safeJsonParse(jsonString, fieldName) {
|
|
|
270
270
|
throw new Error(`${fieldName} contains invalid JSON`);
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
+
// Reference to avoid direct global usage (n8n linter restriction)
|
|
274
|
+
const setTimeoutRef = globalThis.setTimeout;
|
|
273
275
|
/**
|
|
274
276
|
* Pauses execution for a specified duration.
|
|
275
277
|
*
|
|
@@ -282,7 +284,7 @@ function safeJsonParse(jsonString, fieldName) {
|
|
|
282
284
|
* await sleep(1000) // Wait 1 second
|
|
283
285
|
*/
|
|
284
286
|
function sleep(ms) {
|
|
285
|
-
return new Promise((resolve) =>
|
|
287
|
+
return new Promise((resolve) => setTimeoutRef(resolve, ms));
|
|
286
288
|
}
|
|
287
289
|
/**
|
|
288
290
|
* Checks if an error is a rate limit error (HTTP 429).
|
|
@@ -341,6 +343,7 @@ function isRetryableError(error) {
|
|
|
341
343
|
* - Automatic authentication using stored credentials
|
|
342
344
|
* - Rate limit handling with automatic retry after delay
|
|
343
345
|
* - Exponential backoff for server errors (5xx)
|
|
346
|
+
* - Configurable request timeout (default: 30 seconds)
|
|
344
347
|
* - Detailed error messages using NodeApiError
|
|
345
348
|
*
|
|
346
349
|
* @param this - The n8n execution context
|
|
@@ -348,8 +351,9 @@ function isRetryableError(error) {
|
|
|
348
351
|
* @param endpoint - API endpoint path (e.g., '/v1/products')
|
|
349
352
|
* @param body - Optional request body for POST/PATCH requests
|
|
350
353
|
* @param qs - Optional query string parameters
|
|
354
|
+
* @param timeout - Request timeout in milliseconds (default: 30000)
|
|
351
355
|
* @returns The API response data
|
|
352
|
-
* @throws NodeApiError if the request fails after all retries
|
|
356
|
+
* @throws NodeApiError if the request fails after all retries or times out
|
|
353
357
|
*
|
|
354
358
|
* @example
|
|
355
359
|
* // GET request
|
|
@@ -359,13 +363,17 @@ function isRetryableError(error) {
|
|
|
359
363
|
* const checkout = await lemonSqueezyApiRequest.call(this, 'POST', '/v1/checkouts', {
|
|
360
364
|
* data: { type: 'checkouts', attributes: { ... } }
|
|
361
365
|
* })
|
|
366
|
+
*
|
|
367
|
+
* // Request with custom timeout (60 seconds)
|
|
368
|
+
* const data = await lemonSqueezyApiRequest.call(this, 'GET', '/v1/orders', undefined, {}, 60000)
|
|
362
369
|
*/
|
|
363
|
-
async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
|
|
370
|
+
async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}, timeout = constants_1.DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
364
371
|
const options = {
|
|
365
372
|
method,
|
|
366
373
|
url: `${constants_1.API_BASE_URL}${endpoint}`,
|
|
367
374
|
qs,
|
|
368
375
|
json: true,
|
|
376
|
+
timeout,
|
|
369
377
|
};
|
|
370
378
|
if (body) {
|
|
371
379
|
options.body = body;
|
|
@@ -373,18 +381,17 @@ async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
|
|
|
373
381
|
let lastError;
|
|
374
382
|
for (let attempt = 0; attempt < constants_1.MAX_RETRIES; attempt++) {
|
|
375
383
|
try {
|
|
376
|
-
return (await this.helpers.
|
|
384
|
+
return (await this.helpers.httpRequestWithAuthentication.call(this, 'lemonSqueezyApi', options));
|
|
377
385
|
}
|
|
378
386
|
catch (error) {
|
|
379
387
|
lastError = error;
|
|
380
388
|
if (isRateLimitError(error)) {
|
|
381
|
-
// Wait for rate limit to reset (usually 60 seconds)
|
|
382
389
|
await sleep(constants_1.RATE_LIMIT_DELAY_MS);
|
|
383
390
|
continue;
|
|
384
391
|
}
|
|
385
392
|
if (isRetryableError(error) && attempt < constants_1.MAX_RETRIES - 1) {
|
|
386
|
-
|
|
387
|
-
await sleep(
|
|
393
|
+
const delayMs = constants_1.RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
394
|
+
await sleep(delayMs);
|
|
388
395
|
continue;
|
|
389
396
|
}
|
|
390
397
|
// Non-retryable error, throw immediately
|
|
@@ -436,8 +443,8 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginat
|
|
|
436
443
|
const startTime = Date.now();
|
|
437
444
|
qs['page[size]'] = pageSize;
|
|
438
445
|
do {
|
|
439
|
-
// Check timeout
|
|
440
|
-
if (Date.now() - startTime > timeout) {
|
|
446
|
+
// Check timeout (0 = no timeout)
|
|
447
|
+
if (timeout > 0 && Date.now() - startTime > timeout) {
|
|
441
448
|
throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, {
|
|
442
449
|
message: `Pagination timeout exceeded (${timeout}ms). Retrieved ${returnData.length} items before timeout.`,
|
|
443
450
|
});
|
|
@@ -450,7 +457,7 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginat
|
|
|
450
457
|
};
|
|
451
458
|
let responseData;
|
|
452
459
|
try {
|
|
453
|
-
responseData = (await this.helpers.
|
|
460
|
+
responseData = (await this.helpers.httpRequestWithAuthentication.call(this, 'lemonSqueezyApi', options));
|
|
454
461
|
}
|
|
455
462
|
catch (error) {
|
|
456
463
|
if (isRateLimitError(error)) {
|
|
@@ -47,8 +47,43 @@ export declare const RESOURCE_INCLUDES: Record<string, Array<{
|
|
|
47
47
|
name: string;
|
|
48
48
|
value: string;
|
|
49
49
|
}>>;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a filters collection field for a resource
|
|
52
|
+
* @param resource - The resource name
|
|
53
|
+
* @param filterOptions - Array of filter field definitions
|
|
54
|
+
* @param operations - Operations where this field applies
|
|
55
|
+
*/
|
|
56
|
+
export declare function createFiltersField(resource: string, filterOptions: INodeProperties['options'], operations?: string[]): INodeProperties;
|
|
57
|
+
/**
|
|
58
|
+
* Common filter field: Store ID
|
|
59
|
+
*/
|
|
60
|
+
export declare const storeIdFilter: INodeProperties;
|
|
61
|
+
/**
|
|
62
|
+
* Common filter field: Status (generic)
|
|
63
|
+
*/
|
|
64
|
+
export declare function createStatusFilter(statusOptions: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
value: string;
|
|
67
|
+
}>): INodeProperties;
|
|
68
|
+
/**
|
|
69
|
+
* Common filter field: Email
|
|
70
|
+
*/
|
|
71
|
+
export declare const emailFilter: INodeProperties;
|
|
72
|
+
/**
|
|
73
|
+
* Common filter field: Product ID
|
|
74
|
+
*/
|
|
75
|
+
export declare const productIdFilter: INodeProperties;
|
|
76
|
+
/**
|
|
77
|
+
* Common filter field: Variant ID
|
|
78
|
+
*/
|
|
79
|
+
export declare const variantIdFilter: INodeProperties;
|
|
80
|
+
/**
|
|
81
|
+
* Common filter field: Order ID
|
|
82
|
+
*/
|
|
83
|
+
export declare const orderIdFilter: INodeProperties;
|
|
50
84
|
/**
|
|
51
85
|
* Generate advanced options field for a specific resource
|
|
86
|
+
* Includes sorting, relationship expansion, and pagination timeout
|
|
52
87
|
*/
|
|
53
88
|
export declare function createAdvancedOptionsField(resource: string, operations?: string[]): INodeProperties;
|
|
54
89
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.discountAdvancedOptions = exports.checkoutAdvancedOptions = exports.variantAdvancedOptions = exports.productAdvancedOptions = exports.licenseKeyAdvancedOptions = exports.customerAdvancedOptions = exports.subscriptionAdvancedOptions = exports.orderAdvancedOptions = exports.RESOURCE_INCLUDES = exports.COMMON_SORT_FIELDS = exports.SORT_DIRECTIONS = void 0;
|
|
3
|
+
exports.discountAdvancedOptions = exports.checkoutAdvancedOptions = exports.variantAdvancedOptions = exports.productAdvancedOptions = exports.licenseKeyAdvancedOptions = exports.customerAdvancedOptions = exports.subscriptionAdvancedOptions = exports.orderAdvancedOptions = exports.orderIdFilter = exports.variantIdFilter = exports.productIdFilter = exports.emailFilter = exports.storeIdFilter = exports.RESOURCE_INCLUDES = exports.COMMON_SORT_FIELDS = exports.SORT_DIRECTIONS = void 0;
|
|
4
4
|
exports.createReturnAllField = createReturnAllField;
|
|
5
5
|
exports.createLimitField = createLimitField;
|
|
6
6
|
exports.createIdField = createIdField;
|
|
7
|
+
exports.createFiltersField = createFiltersField;
|
|
8
|
+
exports.createStatusFilter = createStatusFilter;
|
|
7
9
|
exports.createAdvancedOptionsField = createAdvancedOptionsField;
|
|
8
10
|
/**
|
|
9
11
|
* Shared field definitions for advanced query options
|
|
@@ -138,8 +140,91 @@ exports.RESOURCE_INCLUDES = {
|
|
|
138
140
|
{ name: 'Discount Redemptions', value: 'discount-redemptions' },
|
|
139
141
|
],
|
|
140
142
|
};
|
|
143
|
+
/**
|
|
144
|
+
* Creates a filters collection field for a resource
|
|
145
|
+
* @param resource - The resource name
|
|
146
|
+
* @param filterOptions - Array of filter field definitions
|
|
147
|
+
* @param operations - Operations where this field applies
|
|
148
|
+
*/
|
|
149
|
+
function createFiltersField(resource, filterOptions, operations = ['getAll']) {
|
|
150
|
+
return {
|
|
151
|
+
displayName: 'Filters',
|
|
152
|
+
name: 'filters',
|
|
153
|
+
type: 'collection',
|
|
154
|
+
placeholder: 'Add Filter',
|
|
155
|
+
default: {},
|
|
156
|
+
displayOptions: {
|
|
157
|
+
show: { resource: [resource], operation: operations },
|
|
158
|
+
},
|
|
159
|
+
options: filterOptions,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Common filter field: Store ID
|
|
164
|
+
*/
|
|
165
|
+
exports.storeIdFilter = {
|
|
166
|
+
displayName: 'Store ID',
|
|
167
|
+
name: 'storeId',
|
|
168
|
+
type: 'string',
|
|
169
|
+
default: '',
|
|
170
|
+
description: 'Filter by store ID',
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Common filter field: Status (generic)
|
|
174
|
+
*/
|
|
175
|
+
function createStatusFilter(statusOptions) {
|
|
176
|
+
return {
|
|
177
|
+
displayName: 'Status',
|
|
178
|
+
name: 'status',
|
|
179
|
+
type: 'options',
|
|
180
|
+
options: statusOptions,
|
|
181
|
+
default: '',
|
|
182
|
+
description: 'Filter by status',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Common filter field: Email
|
|
187
|
+
*/
|
|
188
|
+
exports.emailFilter = {
|
|
189
|
+
displayName: 'Email',
|
|
190
|
+
name: 'email',
|
|
191
|
+
type: 'string',
|
|
192
|
+
default: '',
|
|
193
|
+
description: 'Filter by email address',
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Common filter field: Product ID
|
|
197
|
+
*/
|
|
198
|
+
exports.productIdFilter = {
|
|
199
|
+
displayName: 'Product ID',
|
|
200
|
+
name: 'productId',
|
|
201
|
+
type: 'string',
|
|
202
|
+
default: '',
|
|
203
|
+
description: 'Filter by product ID',
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Common filter field: Variant ID
|
|
207
|
+
*/
|
|
208
|
+
exports.variantIdFilter = {
|
|
209
|
+
displayName: 'Variant ID',
|
|
210
|
+
name: 'variantId',
|
|
211
|
+
type: 'string',
|
|
212
|
+
default: '',
|
|
213
|
+
description: 'Filter by variant ID',
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Common filter field: Order ID
|
|
217
|
+
*/
|
|
218
|
+
exports.orderIdFilter = {
|
|
219
|
+
displayName: 'Order ID',
|
|
220
|
+
name: 'orderId',
|
|
221
|
+
type: 'string',
|
|
222
|
+
default: '',
|
|
223
|
+
description: 'Filter by order ID',
|
|
224
|
+
};
|
|
141
225
|
/**
|
|
142
226
|
* Generate advanced options field for a specific resource
|
|
227
|
+
* Includes sorting, relationship expansion, and pagination timeout
|
|
143
228
|
*/
|
|
144
229
|
function createAdvancedOptionsField(resource, operations = ['getAll']) {
|
|
145
230
|
const includes = exports.RESOURCE_INCLUDES[resource] || [];
|
|
@@ -160,6 +245,14 @@ function createAdvancedOptionsField(resource, operations = ['getAll']) {
|
|
|
160
245
|
default: 'desc',
|
|
161
246
|
description: 'Direction to sort results',
|
|
162
247
|
},
|
|
248
|
+
{
|
|
249
|
+
displayName: 'Pagination Timeout (Seconds)',
|
|
250
|
+
name: 'paginationTimeout',
|
|
251
|
+
type: 'number',
|
|
252
|
+
default: 300,
|
|
253
|
+
description: 'Maximum time in seconds to wait for paginated results when using Return All. Set to 0 for no timeout.',
|
|
254
|
+
typeOptions: { minValue: 0, maxValue: 600 },
|
|
255
|
+
},
|
|
163
256
|
];
|
|
164
257
|
if (includes.length > 0) {
|
|
165
258
|
options.push({
|
|
@@ -13,6 +13,12 @@ exports.usageRecordOperations = [
|
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
options: [
|
|
16
|
+
{
|
|
17
|
+
name: 'Create',
|
|
18
|
+
value: 'create',
|
|
19
|
+
description: 'Create a usage record',
|
|
20
|
+
action: 'Create a usage record',
|
|
21
|
+
},
|
|
16
22
|
{
|
|
17
23
|
name: 'Get',
|
|
18
24
|
value: 'get',
|
|
@@ -30,6 +36,63 @@ exports.usageRecordOperations = [
|
|
|
30
36
|
},
|
|
31
37
|
];
|
|
32
38
|
exports.usageRecordFields = [
|
|
39
|
+
// Create
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Subscription Item ID',
|
|
42
|
+
name: 'subscriptionItemId',
|
|
43
|
+
type: 'string',
|
|
44
|
+
required: true,
|
|
45
|
+
default: '',
|
|
46
|
+
displayOptions: {
|
|
47
|
+
show: {
|
|
48
|
+
resource: ['usageRecord'],
|
|
49
|
+
operation: ['create'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
description: 'The ID of the subscription item to record usage for',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
displayName: 'Quantity',
|
|
56
|
+
name: 'quantity',
|
|
57
|
+
type: 'number',
|
|
58
|
+
required: true,
|
|
59
|
+
default: 1,
|
|
60
|
+
typeOptions: {
|
|
61
|
+
minValue: 1,
|
|
62
|
+
},
|
|
63
|
+
displayOptions: {
|
|
64
|
+
show: {
|
|
65
|
+
resource: ['usageRecord'],
|
|
66
|
+
operation: ['create'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
description: 'The usage quantity to record',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
displayName: 'Action',
|
|
73
|
+
name: 'action',
|
|
74
|
+
type: 'options',
|
|
75
|
+
default: 'increment',
|
|
76
|
+
displayOptions: {
|
|
77
|
+
show: {
|
|
78
|
+
resource: ['usageRecord'],
|
|
79
|
+
operation: ['create'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
options: [
|
|
83
|
+
{
|
|
84
|
+
name: 'Increment',
|
|
85
|
+
value: 'increment',
|
|
86
|
+
description: 'Add to existing usage',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Set',
|
|
90
|
+
value: 'set',
|
|
91
|
+
description: 'Set the usage to an exact value',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
description: 'Whether to increment the existing usage or set it to an exact value',
|
|
95
|
+
},
|
|
33
96
|
// Get
|
|
34
97
|
{
|
|
35
98
|
displayName: 'Usage Record ID',
|
|
@@ -64,7 +64,8 @@ exports.webhookFields = [
|
|
|
64
64
|
type: 'string',
|
|
65
65
|
required: true,
|
|
66
66
|
default: '',
|
|
67
|
-
|
|
67
|
+
placeholder: 'e.g., 12345',
|
|
68
|
+
description: 'The ID of the store this webhook belongs to. Find this in your <a href="https://app.lemonsqueezy.com/settings/stores" target="_blank">Lemon Squeezy Dashboard</a>.',
|
|
68
69
|
displayOptions: {
|
|
69
70
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
70
71
|
},
|
|
@@ -76,7 +77,7 @@ exports.webhookFields = [
|
|
|
76
77
|
required: true,
|
|
77
78
|
default: '',
|
|
78
79
|
placeholder: 'https://your-app.com/webhooks/lemonsqueezy',
|
|
79
|
-
description: 'The URL to
|
|
80
|
+
description: 'The publicly accessible HTTPS URL to receive webhook events. Must be reachable from the internet.',
|
|
80
81
|
displayOptions: {
|
|
81
82
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
82
83
|
},
|
|
@@ -88,7 +89,7 @@ exports.webhookFields = [
|
|
|
88
89
|
required: true,
|
|
89
90
|
options: constants_1.WEBHOOK_EVENTS,
|
|
90
91
|
default: [],
|
|
91
|
-
description: '
|
|
92
|
+
description: 'Select which events should trigger this webhook. See <a href="https://docs.lemonsqueezy.com/api/webhooks#event-types" target="_blank">Lemon Squeezy Webhook Events</a> for details.',
|
|
92
93
|
displayOptions: {
|
|
93
94
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
94
95
|
},
|
|
@@ -100,7 +101,8 @@ exports.webhookFields = [
|
|
|
100
101
|
required: true,
|
|
101
102
|
default: '',
|
|
102
103
|
typeOptions: { password: true },
|
|
103
|
-
|
|
104
|
+
placeholder: 'e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
|
|
105
|
+
description: 'A secure random string (minimum 32 characters) used to sign webhook payloads. Generate one using: openssl rand -hex 32',
|
|
104
106
|
displayOptions: {
|
|
105
107
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
106
108
|
},
|
|
@@ -140,7 +142,8 @@ exports.webhookFields = [
|
|
|
140
142
|
name: 'url',
|
|
141
143
|
type: 'string',
|
|
142
144
|
default: '',
|
|
143
|
-
|
|
145
|
+
placeholder: 'https://your-app.com/webhooks/lemonsqueezy',
|
|
146
|
+
description: 'The publicly accessible HTTPS URL to receive webhook events',
|
|
144
147
|
},
|
|
145
148
|
{
|
|
146
149
|
displayName: 'Events',
|
|
@@ -148,7 +151,7 @@ exports.webhookFields = [
|
|
|
148
151
|
type: 'multiOptions',
|
|
149
152
|
options: constants_1.WEBHOOK_EVENTS,
|
|
150
153
|
default: [],
|
|
151
|
-
description: '
|
|
154
|
+
description: 'Select which events should trigger this webhook. See <a href="https://docs.lemonsqueezy.com/api/webhooks#event-types" target="_blank">Lemon Squeezy docs</a> for details.',
|
|
152
155
|
},
|
|
153
156
|
{
|
|
154
157
|
displayName: 'Secret',
|
|
@@ -156,7 +159,8 @@ exports.webhookFields = [
|
|
|
156
159
|
type: 'string',
|
|
157
160
|
typeOptions: { password: true },
|
|
158
161
|
default: '',
|
|
159
|
-
|
|
162
|
+
placeholder: 'e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
|
|
163
|
+
description: 'A secure random string (minimum 32 characters) used to sign webhook payloads. Generate one using: openssl rand -hex 32',
|
|
160
164
|
},
|
|
161
165
|
],
|
|
162
166
|
},
|