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 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
- - Fixed silent error catching - all errors now logged for debugging
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
- responseData = await helpers_1.lemonSqueezyApiRequestAllItems.call(this, 'GET', `/${endpoint}`, qs);
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 doesn't exist anymore or API error occurred
102
- // Log for debugging but don't fail - we'll recreate the webhook
103
- // eslint-disable-next-line no-console
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 (error) {
125
- // Error checking webhooks - log for debugging
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
- const response = await helpers_1.lemonSqueezyApiRequest.call(this, 'POST', '/webhooks', body);
159
- const responseData = response;
160
- const data = responseData.data;
161
- if (data === null || data === void 0 ? void 0 : data.id) {
162
- webhookData.webhookId = data.id;
163
- return true;
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/${String(webhookData.webhookId)}`);
198
+ await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${webhookId}`);
172
199
  }
173
- catch (error) {
174
- // Webhook might already be deleted or API error
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>): Promise<IDataObject>;
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) => setTimeout(resolve, ms));
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.requestWithAuthentication.call(this, 'lemonSqueezyApi', options));
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
- // Exponential backoff for retryable errors
387
- await sleep(constants_1.RETRY_DELAY_MS * Math.pow(2, attempt));
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.requestWithAuthentication.call(this, 'lemonSqueezyApi', options));
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
- description: 'The ID of the store this webhook belongs to',
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 send webhook events 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: 'The events to subscribe to',
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
- description: 'A secret string used to sign webhook payloads for verification',
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
- description: 'The URL to send webhook events to',
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: 'The events to subscribe to',
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
- description: 'A secret string used to sign webhook payloads',
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-lemonsqueezy",
3
- "version": "0.5.0",
3
+ "version": "0.7.1",
4
4
  "description": "n8n community node for Lemon Squeezy - digital products and subscriptions platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",