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.
- package/README.md +170 -6
- package/dist/nodes/LemonSqueezy/LemonSqueezy.node.js +37 -5
- package/dist/nodes/LemonSqueezy/LemonSqueezyTrigger.node.js +78 -33
- package/dist/nodes/LemonSqueezy/constants.js +11 -2
- package/dist/nodes/LemonSqueezy/helpers.d.ts +346 -7
- package/dist/nodes/LemonSqueezy/helpers.js +542 -16
- package/dist/nodes/LemonSqueezy/resources/discountRedemption.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/discountRedemption.js +109 -0
- package/dist/nodes/LemonSqueezy/resources/index.d.ts +7 -1
- package/dist/nodes/LemonSqueezy/resources/index.js +46 -1
- package/dist/nodes/LemonSqueezy/resources/licenseKeyInstance.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/licenseKeyInstance.js +102 -0
- package/dist/nodes/LemonSqueezy/resources/orderItem.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/orderItem.js +116 -0
- package/dist/nodes/LemonSqueezy/resources/shared.d.ts +64 -0
- package/dist/nodes/LemonSqueezy/resources/shared.js +196 -0
- package/dist/nodes/LemonSqueezy/resources/subscriptionInvoice.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/subscriptionInvoice.js +129 -0
- package/dist/nodes/LemonSqueezy/resources/usageRecord.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/usageRecord.js +102 -0
- package/dist/nodes/LemonSqueezy/resources/user.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/user.js +26 -0
- package/dist/nodes/LemonSqueezy/types.d.ts +63 -7
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 =
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
+
}
|