n8n-nodes-lemonsqueezy 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -311,6 +311,29 @@ Contributions are welcome! Please feel free to submit a Pull Request.
311
311
 
312
312
  ## Changelog
313
313
 
314
+ ### v0.6.0
315
+
316
+ **Reliability & Error Handling:**
317
+ - Improved webhook management error handling with proper 404 vs other error distinction
318
+ - Added detailed logging for webhook lifecycle (create, check, delete operations)
319
+ - Added logging for filtered/unsubscribed webhook events to aid debugging
320
+ - Added rate limit visibility logging with wait time information
321
+ - Added retry attempt logging with exponential backoff details
322
+
323
+ **Input Validation:**
324
+ - Added pre-API validation for email fields (customer create/update, checkout)
325
+ - Added pre-API validation for URL fields (webhook URL, redirect URLs, receipt link URLs)
326
+ - Added webhook secret minimum length validation (16 characters) for security
327
+ - Validation errors now fail fast before making API requests
328
+
329
+ **Performance:**
330
+ - Added configurable request timeout (default: 30 seconds) for all API requests
331
+ - Timeout prevents hanging requests and improves workflow reliability
332
+
333
+ **Code Quality:**
334
+ - Added common filter field generators to reduce code duplication
335
+ - Added createFiltersField, createStatusFilter factory functions
336
+
314
337
  ### v0.5.0
315
338
 
316
339
  **Security & Stability Improvements:**
@@ -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) {
@@ -154,6 +162,12 @@ async function handleCreate(ctx, resource, itemIndex) {
154
162
  const events = ctx.getNodeParameter('webhookEvents', itemIndex);
155
163
  const secret = ctx.getNodeParameter('webhookSecret', itemIndex);
156
164
  const additionalOptions = ctx.getNodeParameter('additionalOptions', itemIndex, {});
165
+ // Validate URL before API call
166
+ (0, helpers_1.validateField)('url', url, 'url');
167
+ // Validate webhook secret minimum length for security
168
+ if (secret.length < 16) {
169
+ throw new Error('Webhook secret must be at least 16 characters for security');
170
+ }
157
171
  const attributes = { url, events, secret };
158
172
  if (additionalOptions.testMode !== undefined) {
159
173
  attributes.test_mode = additionalOptions.testMode;
@@ -202,6 +216,8 @@ async function handleUpdate(ctx, resource, itemIndex) {
202
216
  attributes.name = updateFields.name;
203
217
  }
204
218
  if (updateFields.email) {
219
+ // Validate email before API call
220
+ (0, helpers_1.validateField)('email', updateFields.email, 'email');
205
221
  attributes.email = updateFields.email;
206
222
  }
207
223
  if (updateFields.city) {
@@ -240,12 +256,18 @@ async function handleUpdate(ctx, resource, itemIndex) {
240
256
  const updateFields = ctx.getNodeParameter('updateFields', itemIndex);
241
257
  const attributes = {};
242
258
  if (updateFields.url) {
259
+ // Validate URL before API call
260
+ (0, helpers_1.validateField)('url', updateFields.url, 'url');
243
261
  attributes.url = updateFields.url;
244
262
  }
245
263
  if (updateFields.events) {
246
264
  attributes.events = updateFields.events;
247
265
  }
248
266
  if (updateFields.secret) {
267
+ // Validate webhook secret minimum length for security
268
+ if (updateFields.secret.length < 16) {
269
+ throw new Error('Webhook secret must be at least 16 characters for security');
270
+ }
249
271
  attributes.secret = updateFields.secret;
250
272
  }
251
273
  const body = (0, helpers_1.buildJsonApiBody)('webhooks', attributes, undefined, webhookId);
@@ -90,6 +90,14 @@ class LemonSqueezyTrigger {
90
90
  const webhookUrl = this.getNodeWebhookUrl('default');
91
91
  const storeId = this.getNodeParameter('storeId');
92
92
  const webhookData = this.getWorkflowStaticData('node');
93
+ // Helper to check if error is a 404 (webhook not found)
94
+ const isNotFoundError = (error) => {
95
+ if (error && typeof error === 'object') {
96
+ const err = error;
97
+ return err.statusCode === 404 || err.httpCode === 404 || err.code === 404;
98
+ }
99
+ return false;
100
+ };
93
101
  // Check if we have stored webhook data
94
102
  if (webhookData.webhookId) {
95
103
  try {
@@ -98,10 +106,17 @@ class LemonSqueezyTrigger {
98
106
  return true;
99
107
  }
100
108
  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'}`);
109
+ const webhookId = String(webhookData.webhookId);
110
+ if (isNotFoundError(error)) {
111
+ // Webhook was deleted externally - this is expected, recreate it
112
+ // eslint-disable-next-line no-console
113
+ console.log(`[LemonSqueezy] Webhook ${webhookId} not found (404) - will recreate`);
114
+ }
115
+ else {
116
+ // Unexpected error - could be auth, network, or server issue
117
+ // eslint-disable-next-line no-console
118
+ console.warn(`[LemonSqueezy] Webhook ${webhookId} check failed with unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}. Will attempt to recreate.`);
119
+ }
105
120
  delete webhookData.webhookId;
106
121
  return false;
107
122
  }
@@ -117,15 +132,16 @@ class LemonSqueezyTrigger {
117
132
  const existingWebhook = webhooks.find((webhook) => { var _a; return ((_a = webhook.attributes) === null || _a === void 0 ? void 0 : _a.url) === webhookUrl; });
118
133
  if (existingWebhook) {
119
134
  webhookData.webhookId = existingWebhook.id;
135
+ // eslint-disable-next-line no-console
136
+ console.log(`[LemonSqueezy] Found existing webhook ${String(existingWebhook.id)} for URL ${webhookUrl}`);
120
137
  return true;
121
138
  }
122
139
  }
123
140
  }
124
141
  catch (error) {
125
- // Error checking webhooks - log for debugging
126
- // This could indicate API issues, but we'll try to create a new webhook
142
+ // Error checking webhooks - this is more serious as it could indicate API/auth issues
127
143
  // eslint-disable-next-line no-console
128
- console.debug(`Error checking existing webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`);
144
+ console.error(`[LemonSqueezy] Error checking existing webhooks for store ${storeId}: ${error instanceof Error ? error.message : 'Unknown error'}. Will attempt to create new webhook.`);
129
145
  }
130
146
  return false;
131
147
  },
@@ -167,14 +183,28 @@ class LemonSqueezyTrigger {
167
183
  async delete() {
168
184
  const webhookData = this.getWorkflowStaticData('node');
169
185
  if (webhookData.webhookId) {
186
+ const webhookId = String(webhookData.webhookId);
170
187
  try {
171
- await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${String(webhookData.webhookId)}`);
188
+ await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${webhookId}`);
189
+ // eslint-disable-next-line no-console
190
+ console.log(`[LemonSqueezy] Webhook ${webhookId} deleted successfully`);
172
191
  }
173
192
  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'}`);
193
+ // Check if it's a 404 (already deleted) vs other errors
194
+ const is404 = error &&
195
+ typeof error === 'object' &&
196
+ (error.statusCode === 404 ||
197
+ error.httpCode === 404);
198
+ if (is404) {
199
+ // Webhook was already deleted - this is fine
200
+ // eslint-disable-next-line no-console
201
+ console.log(`[LemonSqueezy] Webhook ${webhookId} already deleted (404)`);
202
+ }
203
+ else {
204
+ // Unexpected error during deletion - log as warning
205
+ // eslint-disable-next-line no-console
206
+ console.warn(`[LemonSqueezy] Webhook ${webhookId} deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}. Continuing cleanup.`);
207
+ }
178
208
  }
179
209
  delete webhookData.webhookId;
180
210
  }
@@ -252,10 +282,13 @@ class LemonSqueezyTrigger {
252
282
  const subscribedEvents = this.getNodeParameter('events');
253
283
  if (!eventName || !subscribedEvents.includes(eventName)) {
254
284
  // Event not subscribed, acknowledge but don't trigger workflow
285
+ // Log for debugging - helps identify misconfigured webhooks
286
+ // eslint-disable-next-line no-console
287
+ console.log(`[LemonSqueezy] Received event '${eventName || 'unknown'}' but not in subscribed events [${subscribedEvents.join(', ')}]. Acknowledged but not processed.`);
255
288
  return {
256
289
  webhookResponse: {
257
290
  status: 200,
258
- body: { received: true, processed: false },
291
+ body: { received: true, processed: false, event: eventName },
259
292
  },
260
293
  };
261
294
  }
@@ -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
  *
@@ -341,6 +341,7 @@ function isRetryableError(error) {
341
341
  * - Automatic authentication using stored credentials
342
342
  * - Rate limit handling with automatic retry after delay
343
343
  * - Exponential backoff for server errors (5xx)
344
+ * - Configurable request timeout (default: 30 seconds)
344
345
  * - Detailed error messages using NodeApiError
345
346
  *
346
347
  * @param this - The n8n execution context
@@ -348,8 +349,9 @@ function isRetryableError(error) {
348
349
  * @param endpoint - API endpoint path (e.g., '/v1/products')
349
350
  * @param body - Optional request body for POST/PATCH requests
350
351
  * @param qs - Optional query string parameters
352
+ * @param timeout - Request timeout in milliseconds (default: 30000)
351
353
  * @returns The API response data
352
- * @throws NodeApiError if the request fails after all retries
354
+ * @throws NodeApiError if the request fails after all retries or times out
353
355
  *
354
356
  * @example
355
357
  * // GET request
@@ -359,13 +361,17 @@ function isRetryableError(error) {
359
361
  * const checkout = await lemonSqueezyApiRequest.call(this, 'POST', '/v1/checkouts', {
360
362
  * data: { type: 'checkouts', attributes: { ... } }
361
363
  * })
364
+ *
365
+ * // Request with custom timeout (60 seconds)
366
+ * const data = await lemonSqueezyApiRequest.call(this, 'GET', '/v1/orders', undefined, {}, 60000)
362
367
  */
363
- async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
368
+ async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}, timeout = constants_1.DEFAULT_REQUEST_TIMEOUT_MS) {
364
369
  const options = {
365
370
  method,
366
371
  url: `${constants_1.API_BASE_URL}${endpoint}`,
367
372
  qs,
368
373
  json: true,
374
+ timeout,
369
375
  };
370
376
  if (body) {
371
377
  options.body = body;
@@ -378,13 +384,17 @@ async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
378
384
  catch (error) {
379
385
  lastError = error;
380
386
  if (isRateLimitError(error)) {
381
- // Wait for rate limit to reset (usually 60 seconds)
387
+ // Log rate limit for visibility
388
+ // eslint-disable-next-line no-console
389
+ console.warn(`[LemonSqueezy] Rate limited (429) on ${method} ${endpoint}. Waiting ${constants_1.RATE_LIMIT_DELAY_MS / 1000}s before retry...`);
382
390
  await sleep(constants_1.RATE_LIMIT_DELAY_MS);
383
391
  continue;
384
392
  }
385
393
  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));
394
+ const delayMs = constants_1.RETRY_DELAY_MS * Math.pow(2, attempt);
395
+ // eslint-disable-next-line no-console
396
+ console.warn(`[LemonSqueezy] Retryable error on ${method} ${endpoint} (attempt ${attempt + 1}/${constants_1.MAX_RETRIES}). Retrying in ${delayMs}ms...`);
397
+ await sleep(delayMs);
388
398
  continue;
389
399
  }
390
400
  // Non-retryable error, throw immediately
@@ -454,6 +464,8 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginat
454
464
  }
455
465
  catch (error) {
456
466
  if (isRateLimitError(error)) {
467
+ // eslint-disable-next-line no-console
468
+ console.warn(`[LemonSqueezy] Rate limited during pagination (${returnData.length} items fetched). Waiting ${constants_1.RATE_LIMIT_DELAY_MS / 1000}s...`);
457
469
  await sleep(constants_1.RATE_LIMIT_DELAY_MS);
458
470
  continue;
459
471
  }
@@ -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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-lemonsqueezy",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "n8n community node for Lemon Squeezy - digital products and subscriptions platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",