n8n-nodes-lemonsqueezy 0.2.0 → 0.5.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.
@@ -1,4 +1,16 @@
1
1
  "use strict";
2
+ /**
3
+ * Lemon Squeezy API Helper Functions
4
+ *
5
+ * This module provides utility functions for:
6
+ * - Input validation (email, URL, date, etc.)
7
+ * - API request handling with retry logic
8
+ * - Webhook signature verification
9
+ * - JSON:API body construction
10
+ * - Query parameter building
11
+ *
12
+ * @module helpers
13
+ */
2
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
15
  if (k2 === undefined) k2 = k;
4
16
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -33,23 +45,257 @@ var __importStar = (this && this.__importStar) || (function () {
33
45
  };
34
46
  })();
35
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.isValidEmail = isValidEmail;
49
+ exports.isValidUrl = isValidUrl;
50
+ exports.isValidIsoDate = isValidIsoDate;
51
+ exports.isPositiveInteger = isPositiveInteger;
52
+ exports.validateField = validateField;
53
+ exports.safeJsonParse = safeJsonParse;
54
+ exports.sleep = sleep;
55
+ exports.isRateLimitError = isRateLimitError;
56
+ exports.isRetryableError = isRetryableError;
36
57
  exports.lemonSqueezyApiRequest = lemonSqueezyApiRequest;
37
58
  exports.lemonSqueezyApiRequestAllItems = lemonSqueezyApiRequestAllItems;
38
59
  exports.validateRequiredFields = validateRequiredFields;
39
60
  exports.buildFilterParams = buildFilterParams;
40
61
  exports.buildJsonApiBody = buildJsonApiBody;
41
62
  exports.verifyWebhookSignature = verifyWebhookSignature;
63
+ exports.buildIncludeParams = buildIncludeParams;
64
+ exports.buildAdvancedFilterParams = buildAdvancedFilterParams;
65
+ exports.extractResponseData = extractResponseData;
66
+ exports.extractIncludedResources = extractIncludedResources;
42
67
  const crypto = __importStar(require("crypto"));
43
68
  const n8n_workflow_1 = require("n8n-workflow");
44
69
  const constants_1 = require("./constants");
70
+ // ============================================================================
71
+ // Validation Helpers
72
+ // ============================================================================
73
+ /**
74
+ * Validates email format using RFC 5322 compliant regex.
75
+ *
76
+ * The validation checks for:
77
+ * - Valid local part characters (alphanumeric and special chars)
78
+ * - Single @ symbol
79
+ * - Valid domain with proper TLD (at least one dot)
80
+ *
81
+ * @param email - The email address to validate
82
+ * @returns True if the email format is valid, false otherwise
83
+ *
84
+ * @example
85
+ * isValidEmail('user@example.com') // true
86
+ * isValidEmail('invalid') // false
87
+ * isValidEmail('user@localhost') // false (no TLD)
88
+ */
89
+ function isValidEmail(email) {
90
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
91
+ return emailRegex.test(email);
92
+ }
93
+ /**
94
+ * Validates URL format and ensures it's a safe external URL.
95
+ *
96
+ * Security features:
97
+ * - Only allows http:// and https:// protocols
98
+ * - Blocks localhost and loopback addresses (127.0.0.1, ::1, [::1])
99
+ * - Blocks private network ranges (10.x, 172.16-31.x, 192.168.x)
100
+ * - Blocks link-local addresses (169.254.x - AWS metadata endpoint)
101
+ *
102
+ * This prevents Server-Side Request Forgery (SSRF) attacks.
103
+ *
104
+ * @param url - The URL to validate
105
+ * @returns True if the URL is valid and safe, false otherwise
106
+ *
107
+ * @example
108
+ * isValidUrl('https://example.com') // true
109
+ * isValidUrl('http://localhost:3000') // false (internal)
110
+ * isValidUrl('ftp://files.example.com') // false (non-http protocol)
111
+ * isValidUrl('http://169.254.169.254') // false (AWS metadata)
112
+ */
113
+ function isValidUrl(url) {
114
+ try {
115
+ const parsedUrl = new URL(url);
116
+ // Only allow http and https protocols (security: prevent file://, javascript:, etc.)
117
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
118
+ return false;
119
+ }
120
+ // Block internal/private network URLs for SSRF protection
121
+ const hostname = parsedUrl.hostname.toLowerCase();
122
+ const blockedPatterns = [
123
+ 'localhost', // Loopback hostname
124
+ '127.0.0.1', // IPv4 loopback
125
+ '0.0.0.0', // All interfaces
126
+ '::1', // IPv6 loopback
127
+ '[::1]', // IPv6 loopback (bracketed form)
128
+ '10.', // Private Class A (10.0.0.0/8)
129
+ '172.16.', // Private Class B start (172.16.0.0/12)
130
+ '172.17.',
131
+ '172.18.',
132
+ '172.19.',
133
+ '172.20.',
134
+ '172.21.',
135
+ '172.22.',
136
+ '172.23.',
137
+ '172.24.',
138
+ '172.25.',
139
+ '172.26.',
140
+ '172.27.',
141
+ '172.28.',
142
+ '172.29.',
143
+ '172.30.',
144
+ '172.31.', // Private Class B end
145
+ '192.168.', // Private Class C (192.168.0.0/16)
146
+ '169.254.', // Link-local / APIPA (includes AWS metadata endpoint)
147
+ ];
148
+ for (const pattern of blockedPatterns) {
149
+ if (hostname === pattern || hostname.startsWith(pattern)) {
150
+ return false;
151
+ }
152
+ }
153
+ return true;
154
+ }
155
+ catch {
156
+ // URL parsing failed - invalid URL
157
+ return false;
158
+ }
159
+ }
160
+ /**
161
+ * Validates ISO 8601 date format.
162
+ *
163
+ * Accepts dates in formats like:
164
+ * - 2024-01-15
165
+ * - 2024-01-15T10:30:00Z
166
+ * - 2024-01-15T10:30:00.000Z
167
+ *
168
+ * @param dateString - The date string to validate
169
+ * @returns True if the date is valid ISO 8601 format, false otherwise
170
+ *
171
+ * @example
172
+ * isValidIsoDate('2024-01-15T10:30:00Z') // true
173
+ * isValidIsoDate('invalid') // false
174
+ * isValidIsoDate('01/15/2024') // false (no dash separator)
175
+ */
176
+ function isValidIsoDate(dateString) {
177
+ const date = new Date(dateString);
178
+ return !isNaN(date.getTime()) && dateString.includes('-');
179
+ }
45
180
  /**
46
- * Sleep for a specified number of milliseconds
181
+ * Validates that a value is a positive integer (greater than 0).
182
+ *
183
+ * @param value - The value to validate
184
+ * @returns True if the value is a positive integer, false otherwise
185
+ *
186
+ * @example
187
+ * isPositiveInteger(5) // true
188
+ * isPositiveInteger(0) // false
189
+ * isPositiveInteger(-1) // false
190
+ * isPositiveInteger(3.14) // false
191
+ * isPositiveInteger('5') // false (string, not number)
192
+ */
193
+ function isPositiveInteger(value) {
194
+ return typeof value === 'number' && Number.isInteger(value) && value > 0;
195
+ }
196
+ /**
197
+ * Validates a field value and throws a descriptive error if invalid.
198
+ *
199
+ * Supports multiple validation types:
200
+ * - 'required': Ensures value is not empty/null/undefined
201
+ * - 'email': RFC 5322 compliant email validation
202
+ * - 'url': Safe URL validation with SSRF protection
203
+ * - 'date': ISO 8601 date format validation
204
+ * - 'positiveInteger': Positive integer validation
205
+ *
206
+ * @param fieldName - The name of the field (used in error messages)
207
+ * @param value - The value to validate
208
+ * @param validationType - The type of validation to perform
209
+ * @throws Error with descriptive message if validation fails
210
+ *
211
+ * @example
212
+ * validateField('email', 'user@example.com', 'email') // passes
213
+ * validateField('email', 'invalid', 'email') // throws "email must be a valid email address"
214
+ */
215
+ function validateField(fieldName, value, validationType) {
216
+ if (validationType === 'required') {
217
+ if (value === undefined || value === null || value === '') {
218
+ throw new Error(`${fieldName} is required`);
219
+ }
220
+ return;
221
+ }
222
+ // Skip validation if value is empty (use 'required' for mandatory fields)
223
+ if (value === undefined || value === null || value === '') {
224
+ return;
225
+ }
226
+ switch (validationType) {
227
+ case 'email':
228
+ if (typeof value !== 'string' || !isValidEmail(value)) {
229
+ throw new Error(`${fieldName} must be a valid email address`);
230
+ }
231
+ break;
232
+ case 'url':
233
+ if (typeof value !== 'string' || !isValidUrl(value)) {
234
+ throw new Error(`${fieldName} must be a valid URL`);
235
+ }
236
+ break;
237
+ case 'date':
238
+ if (typeof value !== 'string' || !isValidIsoDate(value)) {
239
+ throw new Error(`${fieldName} must be a valid ISO 8601 date`);
240
+ }
241
+ break;
242
+ case 'positiveInteger':
243
+ if (!isPositiveInteger(value)) {
244
+ throw new Error(`${fieldName} must be a positive integer`);
245
+ }
246
+ break;
247
+ }
248
+ }
249
+ /**
250
+ * Safely parses a JSON string with descriptive error handling.
251
+ *
252
+ * @template T - The expected type of the parsed JSON
253
+ * @param jsonString - The JSON string to parse
254
+ * @param fieldName - The name of the field (used in error messages)
255
+ * @returns The parsed JSON object
256
+ * @throws Error if the JSON is invalid
257
+ *
258
+ * @example
259
+ * const data = safeJsonParse<{name: string}>('{"name": "test"}', 'config')
260
+ * // Returns: {name: "test"}
261
+ *
262
+ * safeJsonParse('invalid json', 'config')
263
+ * // Throws: "config contains invalid JSON"
264
+ */
265
+ function safeJsonParse(jsonString, fieldName) {
266
+ try {
267
+ return JSON.parse(jsonString);
268
+ }
269
+ catch {
270
+ throw new Error(`${fieldName} contains invalid JSON`);
271
+ }
272
+ }
273
+ /**
274
+ * Pauses execution for a specified duration.
275
+ *
276
+ * Used for implementing retry delays and rate limit backoff.
277
+ *
278
+ * @param ms - The number of milliseconds to sleep
279
+ * @returns A promise that resolves after the specified duration
280
+ *
281
+ * @example
282
+ * await sleep(1000) // Wait 1 second
47
283
  */
48
284
  function sleep(ms) {
49
285
  return new Promise((resolve) => setTimeout(resolve, ms));
50
286
  }
51
287
  /**
52
- * Check if error is a rate limit error
288
+ * Checks if an error is a rate limit error (HTTP 429).
289
+ *
290
+ * Handles both direct statusCode and nested response.statusCode patterns.
291
+ *
292
+ * @param error - The error object to check
293
+ * @returns True if the error is a rate limit error, false otherwise
294
+ *
295
+ * @example
296
+ * isRateLimitError({ statusCode: 429 }) // true
297
+ * isRateLimitError({ response: { statusCode: 429 } }) // true
298
+ * isRateLimitError({ statusCode: 500 }) // false
53
299
  */
54
300
  function isRateLimitError(error) {
55
301
  var _a;
@@ -60,7 +306,19 @@ function isRateLimitError(error) {
60
306
  return false;
61
307
  }
62
308
  /**
63
- * Check if error is retryable (5xx errors or network errors)
309
+ * Checks if an error is retryable (5xx server errors or network errors).
310
+ *
311
+ * Retryable conditions:
312
+ * - HTTP 5xx status codes (500-599)
313
+ * - Network errors: ECONNRESET, ETIMEDOUT, ECONNREFUSED
314
+ *
315
+ * @param error - The error object to check
316
+ * @returns True if the error is retryable, false otherwise
317
+ *
318
+ * @example
319
+ * isRetryableError({ statusCode: 503 }) // true (server error)
320
+ * isRetryableError({ code: 'ECONNRESET' }) // true (network error)
321
+ * isRetryableError({ statusCode: 404 }) // false (client error)
64
322
  */
65
323
  function isRetryableError(error) {
66
324
  var _a;
@@ -77,7 +335,30 @@ function isRetryableError(error) {
77
335
  return false;
78
336
  }
79
337
  /**
80
- * Make an authenticated request to the Lemon Squeezy API with retry logic
338
+ * Makes an authenticated request to the Lemon Squeezy API with retry logic.
339
+ *
340
+ * Features:
341
+ * - Automatic authentication using stored credentials
342
+ * - Rate limit handling with automatic retry after delay
343
+ * - Exponential backoff for server errors (5xx)
344
+ * - Detailed error messages using NodeApiError
345
+ *
346
+ * @param this - The n8n execution context
347
+ * @param method - HTTP method (GET, POST, PATCH, DELETE)
348
+ * @param endpoint - API endpoint path (e.g., '/v1/products')
349
+ * @param body - Optional request body for POST/PATCH requests
350
+ * @param qs - Optional query string parameters
351
+ * @returns The API response data
352
+ * @throws NodeApiError if the request fails after all retries
353
+ *
354
+ * @example
355
+ * // GET request
356
+ * const product = await lemonSqueezyApiRequest.call(this, 'GET', '/v1/products/123')
357
+ *
358
+ * // POST request with body
359
+ * const checkout = await lemonSqueezyApiRequest.call(this, 'POST', '/v1/checkouts', {
360
+ * data: { type: 'checkouts', attributes: { ... } }
361
+ * })
81
362
  */
82
363
  async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
83
364
  const options = {
@@ -118,14 +399,49 @@ async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}) {
118
399
  });
119
400
  }
120
401
  /**
121
- * Make paginated requests to fetch all items
402
+ * Makes paginated requests to fetch all items from a Lemon Squeezy API endpoint.
403
+ *
404
+ * Automatically handles pagination by following 'next' links until all items
405
+ * are retrieved. Includes rate limit handling for long-running fetches.
406
+ *
407
+ * Features:
408
+ * - Optional maxItems limit to prevent memory issues with large datasets
409
+ * - Optional timeout to prevent long-running requests
410
+ * - Rate limit handling with automatic retry
411
+ *
412
+ * @param this - The n8n execution context
413
+ * @param method - HTTP method (typically 'GET')
414
+ * @param endpoint - API endpoint path (e.g., '/v1/products')
415
+ * @param qs - Optional query string parameters (filters, sorting, etc.)
416
+ * @param paginationOptions - Optional pagination configuration
417
+ * @returns Array of all items from all pages (up to maxItems if specified)
418
+ * @throws NodeApiError if any request fails or timeout is exceeded
419
+ *
420
+ * @example
421
+ * // Fetch all products with filtering
422
+ * const products = await lemonSqueezyApiRequestAllItems.call(
423
+ * this, 'GET', '/v1/products', { 'filter[store_id]': 123 }
424
+ * )
425
+ *
426
+ * // Fetch with limits
427
+ * const products = await lemonSqueezyApiRequestAllItems.call(
428
+ * this, 'GET', '/v1/products', {}, { maxItems: 1000, timeout: 60000 }
429
+ * )
122
430
  */
123
- async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}) {
431
+ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginationOptions = {}) {
124
432
  var _a;
125
433
  const returnData = [];
126
434
  let nextPageUrl = `${constants_1.API_BASE_URL}${endpoint}`;
127
- qs['page[size]'] = constants_1.DEFAULT_PAGE_SIZE;
435
+ const { maxItems, timeout = 300000, pageSize = constants_1.DEFAULT_PAGE_SIZE } = paginationOptions;
436
+ const startTime = Date.now();
437
+ qs['page[size]'] = pageSize;
128
438
  do {
439
+ // Check timeout
440
+ if (Date.now() - startTime > timeout) {
441
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, {
442
+ message: `Pagination timeout exceeded (${timeout}ms). Retrieved ${returnData.length} items before timeout.`,
443
+ });
444
+ }
129
445
  const options = {
130
446
  method,
131
447
  url: nextPageUrl,
@@ -145,26 +461,59 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}) {
145
461
  message: getErrorMessage(error),
146
462
  });
147
463
  }
148
- returnData.push(...responseData.data);
464
+ const pageData = responseData.data;
465
+ returnData.push(...pageData);
466
+ // Check maxItems limit
467
+ if (maxItems && returnData.length >= maxItems) {
468
+ return returnData.slice(0, maxItems);
469
+ }
149
470
  nextPageUrl = ((_a = responseData.links) === null || _a === void 0 ? void 0 : _a.next) || null;
150
471
  } while (nextPageUrl);
151
472
  return returnData;
152
473
  }
153
474
  /**
154
- * Extract error message from error object
475
+ * Lemon Squeezy API error codes and their meanings
476
+ */
477
+ const ERROR_MESSAGES = {
478
+ 400: 'Bad Request: The request was invalid or malformed',
479
+ 401: 'Unauthorized: Invalid or missing API key',
480
+ 403: 'Forbidden: You do not have permission to access this resource',
481
+ 404: 'Not Found: The requested resource does not exist',
482
+ 409: 'Conflict: The resource already exists or there is a conflict',
483
+ 422: 'Unprocessable Entity: The request data is invalid',
484
+ 429: 'Rate Limited: Too many requests. Please wait before retrying',
485
+ 500: 'Internal Server Error: Something went wrong on the server',
486
+ 502: 'Bad Gateway: The server is temporarily unavailable',
487
+ 503: 'Service Unavailable: The API is temporarily unavailable',
488
+ };
489
+ /**
490
+ * Extract detailed error message from error object
155
491
  */
156
492
  function getErrorMessage(error) {
157
493
  var _a, _b, _c, _d, _e;
158
494
  if (error && typeof error === 'object') {
159
495
  const err = error;
496
+ const statusCode = err.statusCode || ((_a = err.response) === null || _a === void 0 ? void 0 : _a.statusCode);
160
497
  // Check for JSON:API error format
161
- if ((_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.errors) === null || _c === void 0 ? void 0 : _c[0]) {
162
- const apiError = err.response.body.errors[0];
163
- return apiError.detail || apiError.title || 'Unknown API error';
498
+ if (((_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.body) === null || _c === void 0 ? void 0 : _c.errors) && err.response.body.errors.length > 0) {
499
+ const apiErrors = err.response.body.errors;
500
+ const errorMessages = apiErrors.map((e) => {
501
+ var _a;
502
+ let msg = e.detail || e.title || 'Unknown error';
503
+ if ((_a = e.source) === null || _a === void 0 ? void 0 : _a.pointer) {
504
+ msg += ` (field: ${e.source.pointer.replace('/data/attributes/', '')})`;
505
+ }
506
+ return msg;
507
+ });
508
+ return errorMessages.join('; ');
164
509
  }
165
510
  if ((_e = (_d = err.response) === null || _d === void 0 ? void 0 : _d.body) === null || _e === void 0 ? void 0 : _e.message) {
166
511
  return err.response.body.message;
167
512
  }
513
+ // Use status code mapping
514
+ if (statusCode && ERROR_MESSAGES[statusCode]) {
515
+ return ERROR_MESSAGES[statusCode];
516
+ }
168
517
  if (err.message) {
169
518
  return err.message;
170
519
  }
@@ -172,7 +521,15 @@ function getErrorMessage(error) {
172
521
  return 'An unknown error occurred';
173
522
  }
174
523
  /**
175
- * Validate required fields before making API request
524
+ * Validates that all required fields are present and non-empty.
525
+ *
526
+ * @param fields - Object containing field values to validate
527
+ * @param requiredFields - Array of field names that are required
528
+ * @throws Error listing all missing fields if any are empty
529
+ *
530
+ * @example
531
+ * validateRequiredFields({ name: 'Test', email: '' }, ['name', 'email'])
532
+ * // Throws: "Missing required fields: email"
176
533
  */
177
534
  function validateRequiredFields(fields, requiredFields) {
178
535
  const missingFields = [];
@@ -186,7 +543,17 @@ function validateRequiredFields(fields, requiredFields) {
186
543
  }
187
544
  }
188
545
  /**
189
- * Build filter query string parameters
546
+ * Builds filter query string parameters for Lemon Squeezy API.
547
+ *
548
+ * Converts camelCase field names to snake_case and wraps them in
549
+ * filter[] syntax as required by the API.
550
+ *
551
+ * @param filters - Object containing filter key-value pairs
552
+ * @returns Query string parameters object for API request
553
+ *
554
+ * @example
555
+ * buildFilterParams({ storeId: 123, status: 'active' })
556
+ * // Returns: { 'filter[store_id]': 123, 'filter[status]': 'active' }
190
557
  */
191
558
  function buildFilterParams(filters) {
192
559
  const qs = {};
@@ -200,7 +567,24 @@ function buildFilterParams(filters) {
200
567
  return qs;
201
568
  }
202
569
  /**
203
- * Build JSON:API request body
570
+ * Builds a JSON:API compliant request body for create/update operations.
571
+ *
572
+ * Constructs the proper structure expected by Lemon Squeezy API:
573
+ * - data.type: Resource type (e.g., 'checkouts', 'customers')
574
+ * - data.attributes: Resource attributes
575
+ * - data.relationships: Optional related resource references
576
+ * - data.id: Optional resource ID (for updates)
577
+ *
578
+ * @param type - The JSON:API resource type
579
+ * @param attributes - Resource attributes to include
580
+ * @param relationships - Optional relationships to other resources
581
+ * @param id - Optional resource ID (required for updates)
582
+ * @returns Properly structured JSON:API request body
583
+ *
584
+ * @example
585
+ * buildJsonApiBody('customers', { name: 'John', email: 'john@example.com' },
586
+ * { store: { type: 'stores', id: '123' } })
587
+ * // Returns: { data: { type: 'customers', attributes: {...}, relationships: {...} } }
204
588
  */
205
589
  function buildJsonApiBody(type, attributes, relationships, id) {
206
590
  const body = {
@@ -227,7 +611,21 @@ function buildJsonApiBody(type, attributes, relationships, id) {
227
611
  return body;
228
612
  }
229
613
  /**
230
- * Parse webhook signature for validation
614
+ * Verifies a webhook signature using HMAC-SHA256.
615
+ *
616
+ * Uses timing-safe comparison to prevent timing attacks.
617
+ *
618
+ * @param payload - The raw webhook payload string
619
+ * @param signature - The signature from X-Signature header
620
+ * @param secret - The webhook signing secret
621
+ * @returns True if signature is valid, false otherwise
622
+ *
623
+ * @example
624
+ * const isValid = verifyWebhookSignature(
625
+ * '{"data": {...}}',
626
+ * 'abc123signature',
627
+ * 'webhook_secret_key'
628
+ * )
231
629
  */
232
630
  function verifyWebhookSignature(payload, signature, secret) {
233
631
  const hmac = crypto.createHmac('sha256', secret);
@@ -239,3 +637,131 @@ function verifyWebhookSignature(payload, signature, secret) {
239
637
  return false;
240
638
  }
241
639
  }
640
+ // ============================================================================
641
+ // Advanced Query Helpers
642
+ // ============================================================================
643
+ /**
644
+ * Builds query parameters for including related resources in API responses.
645
+ *
646
+ * Uses the JSON:API include parameter to fetch related resources in a single
647
+ * request, reducing the number of API calls needed.
648
+ *
649
+ * @param includes - Array of relationship names to include
650
+ * @returns Query string parameters object with 'include' key
651
+ *
652
+ * @example
653
+ * buildIncludeParams(['store', 'customer', 'order-items'])
654
+ * // Returns: { include: 'store,customer,order-items' }
655
+ *
656
+ * buildIncludeParams([])
657
+ * // Returns: {}
658
+ */
659
+ function buildIncludeParams(includes) {
660
+ if (includes.length === 0) {
661
+ return {};
662
+ }
663
+ return { include: includes.join(',') };
664
+ }
665
+ /**
666
+ * Builds advanced filter parameters with support for date ranges and sorting.
667
+ *
668
+ * Features:
669
+ * - Converts camelCase to snake_case for API compatibility
670
+ * - Handles date range filters with _after/_before suffixes
671
+ * - Adds sorting with ascending/descending direction
672
+ *
673
+ * @param filters - Object containing filter key-value pairs
674
+ * @param options - Optional configuration for date fields and sorting
675
+ * @param options.dateFields - Array of field names that are date ranges
676
+ * @param options.sortField - Field name to sort by
677
+ * @param options.sortDirection - Sort direction ('asc' or 'desc')
678
+ * @returns Query string parameters object for API request
679
+ *
680
+ * @example
681
+ * buildAdvancedFilterParams(
682
+ * { status: 'active', createdAt: { from: '2024-01-01', to: '2024-12-31' } },
683
+ * { dateFields: ['createdAt'], sortField: 'created_at', sortDirection: 'desc' }
684
+ * )
685
+ * // Returns: { 'filter[status]': 'active', 'filter[created_at_after]': '2024-01-01',
686
+ * // 'filter[created_at_before]': '2024-12-31', sort: '-created_at' }
687
+ */
688
+ function buildAdvancedFilterParams(filters, options) {
689
+ var _a;
690
+ const qs = {};
691
+ for (const [key, value] of Object.entries(filters)) {
692
+ if (value === undefined || value === null || value === '') {
693
+ continue;
694
+ }
695
+ // Convert camelCase to snake_case for API
696
+ const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
697
+ // Handle date range filters
698
+ if ((_a = options === null || options === void 0 ? void 0 : options.dateFields) === null || _a === void 0 ? void 0 : _a.includes(key)) {
699
+ if (typeof value === 'object' && value !== null) {
700
+ const dateRange = value;
701
+ if (dateRange.from) {
702
+ qs[`filter[${snakeKey}_after]`] = dateRange.from;
703
+ }
704
+ if (dateRange.to) {
705
+ qs[`filter[${snakeKey}_before]`] = dateRange.to;
706
+ }
707
+ }
708
+ else {
709
+ qs[`filter[${snakeKey}]`] = value;
710
+ }
711
+ }
712
+ else {
713
+ qs[`filter[${snakeKey}]`] = value;
714
+ }
715
+ }
716
+ // Add sorting
717
+ if (options === null || options === void 0 ? void 0 : options.sortField) {
718
+ const sortPrefix = options.sortDirection === 'desc' ? '-' : '';
719
+ const snakeSortField = options.sortField.replace(/([A-Z])/g, '_$1').toLowerCase();
720
+ qs.sort = `${sortPrefix}${snakeSortField}`;
721
+ }
722
+ return qs;
723
+ }
724
+ /**
725
+ * Extracts the 'data' field from a JSON:API response with proper typing.
726
+ *
727
+ * JSON:API responses wrap the actual resource data in a 'data' field.
728
+ * This helper extracts it while preserving type information.
729
+ *
730
+ * @template T - The expected type of the extracted data
731
+ * @param response - The full JSON:API response object
732
+ * @returns The extracted data, or undefined if not present
733
+ *
734
+ * @example
735
+ * const response = { data: { id: '1', type: 'products', attributes: {...} } }
736
+ * const product = extractResponseData<Product>(response)
737
+ * // Returns: { id: '1', type: 'products', attributes: {...} }
738
+ */
739
+ function extractResponseData(response) {
740
+ if (!response || typeof response !== 'object') {
741
+ return undefined;
742
+ }
743
+ return response.data;
744
+ }
745
+ /**
746
+ * Extracts included related resources from a JSON:API response.
747
+ *
748
+ * When using the 'include' query parameter, related resources are returned
749
+ * in the 'included' array of the response. This helper extracts them.
750
+ *
751
+ * @param response - The full JSON:API response object
752
+ * @returns Array of included resources, or empty array if none
753
+ *
754
+ * @example
755
+ * const response = {
756
+ * data: {...},
757
+ * included: [{ id: '1', type: 'stores', attributes: {...} }]
758
+ * }
759
+ * const stores = extractIncludedResources(response)
760
+ * // Returns: [{ id: '1', type: 'stores', attributes: {...} }]
761
+ */
762
+ function extractIncludedResources(response) {
763
+ if (!response || typeof response !== 'object') {
764
+ return [];
765
+ }
766
+ return response.included || [];
767
+ }
@@ -0,0 +1,3 @@
1
+ import type { INodeProperties } from 'n8n-workflow';
2
+ export declare const discountRedemptionOperations: INodeProperties[];
3
+ export declare const discountRedemptionFields: INodeProperties[];