UNPKG

@inkress/admin-sdk

Version:

Official Inkress Commerce API SDK for JavaScript/TypeScript

1,575 lines (1,568 loc) 143 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var fetch = require('cross-fetch'); class HttpClient { constructor(config) { // Compute endpoint from mode const endpoint = config.mode === 'sandbox' ? 'https://api-dev.inkress.com' : 'https://api.inkress.com'; let mode = 'live'; if (config.accessToken.includes('_test_')) { mode = 'sandbox'; } this.config = { accessToken: config.accessToken, mode: config.mode || mode, apiVersion: config.apiVersion || 'v1', username: config.username || '', timeout: config.timeout || 30000, retries: config.retries || 0, headers: config.headers || {}, endpoint, // computed from mode }; } getBaseUrl() { const { endpoint, apiVersion } = this.config; return `${endpoint}/api/${apiVersion}`; } getHeaders(additionalHeaders = {}) { const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.accessToken}`, ...this.config.headers, ...additionalHeaders, }; // Add Client-Id header if username is provided (prepend with 'm-') if (this.config.username) { headers['Client-Id'] = `m-${this.config.username}`; } return headers; } async makeRequest(path, options = {}) { const url = `${this.getBaseUrl()}${path}`; const { method = 'GET', body, headers: requestHeaders, timeout } = options; const headers = this.getHeaders(requestHeaders); const requestTimeout = timeout || this.config.timeout; const requestInit = { method, headers, }; if (body && method !== 'GET') { requestInit.body = typeof body === 'string' ? body : JSON.stringify(body); } // Create timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Request timeout')), requestTimeout); }); try { const response = await Promise.race([ fetch(url, requestInit), timeoutPromise, ]); if (!response.ok) { const errorText = await response.text(); let errorData; try { errorData = JSON.parse(errorText); } catch (_a) { errorData = { message: errorText || `HTTP ${response.status}` }; } throw new InkressApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData); } const responseText = await response.text(); if (!responseText) { return { state: 'ok', result: undefined }; } const data = JSON.parse(responseText); return data; } catch (error) { if (error instanceof InkressApiError) { throw error; } throw new InkressApiError(error instanceof Error ? error.message : 'Unknown error', 0, { error }); } } async retryRequest(path, options = {}, retries = this.config.retries) { try { return await this.makeRequest(path, options); } catch (error) { if (retries > 0 && this.shouldRetry(error)) { await this.delay(1000 * (this.config.retries - retries + 1)); return this.retryRequest(path, options, retries - 1); } throw error; } } shouldRetry(error) { if (error instanceof InkressApiError) { // Retry on 5xx errors and timeouts return error.status >= 500 || error.status === 0; } return false; } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async get(path, params) { let url = path; if (params) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } }); const queryString = searchParams.toString(); if (queryString) { url += `?${queryString}`; } } return this.retryRequest(url, { method: 'GET' }); } async post(path, body) { return this.retryRequest(path, { method: 'POST', body }); } async put(path, body) { return this.retryRequest(path, { method: 'PUT', body }); } async delete(path) { return this.retryRequest(path, { method: 'DELETE' }); } async patch(path, body) { return this.retryRequest(path, { method: 'PATCH', body }); } // Update configuration updateConfig(newConfig) { // Recompute endpoint if mode changes if (newConfig.mode) { const endpoint = newConfig.mode === 'sandbox' ? 'https://api-dev.inkress.com' : 'https://api.inkress.com'; this.config = { ...this.config, ...newConfig, endpoint }; } else { this.config = { ...this.config, ...newConfig }; } } // Get current configuration (without sensitive data) getConfig() { const { accessToken, ...config } = this.config; // Remove computed endpoint from config const { endpoint, ...publicConfig } = config; return publicConfig; } } class InkressApiError extends Error { constructor(message, status, result) { super(message); this.name = 'InkressApiError'; this.status = status; this.result = result; } } const mappings = { "Access": { "view": 1, "list": 2, "create": 3, "update": 4, "delete": 5 }, "FeeStructure": { "merchant_absorb": 1, "customer_pay": 2 }, "Kind": { "order_online": 1, "order_payment_link": 1, "order_cart": 2, "order_subscription": 3, "order_invoice": 4, "order_offline": 5, "template_email": 1, "template_sms": 2, "template_receipt": 3, "password_account": 4, "password_otp": 5, "legal_request_account_removal": 6, "legal_request_account_report": 7, "notification_sale": 8, "notification_invite": 9, "notification_registration": 10, "notification_account": 11, "notification_report": 12, "notification_auth": 13, "notification_cart_reminder": 14, "notification_product_reminder": 15, "notification_purchase_confirmation": 16, "notification_shipping_confirmation": 17, "notification_delivery_confirmation": 18, "notification_feedback_request": 19, "notification_review_request": 20, "notification_platform_announcement": 21, "notification_organisation_announcement": 22, "notification_store_announcement": 23, "notification_organisation_suggestion": 24, "notification_store_suggestion": 25, "notification_organisation_referral": 26, "notification_store_referral": 27, "notification_organisation_upsell": 28, "notification_store_upsell": 29, "transaction_order": 30, "transaction_payout": 31, "transaction_manual": 32, "transaction_fee": 33, "token_login": 24, "token_api": 25, "token_sso": 32, "token_preset": 33, "user_address": 35, "merchant_address": 36, "organisation_address": 37, "role_preset": 26, "role_organisation": 27, "role_store": 28, "product_draft": 29, "product_published": 30, "product_archived": 31, "file_business_logo": 51, "file_business_document": 50, "file_payout_document": 71, "identity_email": 52, "identity_phone": 53, "billing_plan_subscription": 1, "billing_plan_payout": 2, "billing_subscription_manual_charge": 1, "billing_subscription_auto_charge": 2, "ledger_entry_credit": 1, "ledger_entry_debit": 2, "ledger_payout_standard": 1, "ledger_payout_early": 2, "ledger_payout_manual": 10, "fee_transaction_platform": 1, "fee_transaction_provider": 2, "fee_transaction_tax": 3, "fee_transaction_discount": 4, "fee_transaction_shipping": 5, "fee_transaction_processing": 6, "fee_transaction_subscription": 7, "fee_transaction_payout": 8, "fee_transaction_refund": 9, "fee_transaction_adjustment": 10, "fee_merchant_daily_limit": 11, "fee_merchant_weekly_limit": 12, "fee_merchant_monthly_limit": 13, "fee_merchant_single_limit": 14, "fee_merchant_withdrawal_limit": 15, "legal_request_document_submission": 1, "legal_request_bank_info_update": 2, "legal_request_limit_increase": 3, "payment_link_order": 1, "payment_link_invoice": 2 }, "Status": { "order_pending": 1, "order_error": 2, "order_failed": 2, "order_paid": 3, "order_partial": 32, "order_confirmed": 4, "order_cancelled": 5, "order_prepared": 6, "order_shipped": 7, "order_delivered": 8, "order_completed": 9, "order_returned": 10, "order_refunded": 11, "order_verifying": 12, "order_stale": 13, "order_archived": 14, "transaction_pending": 1, "transaction_authorized": 2, "transaction_hold": 3, "transaction_captured": 4, "transaction_voided": 5, "transaction_refunded": 6, "transaction_processed": 7, "transaction_processing": 8, "transaction_cancelled": 9, "transaction_failed": 10, "transaction_credit": 11, "transaction_debit": 12, "account_unverified": 20, "account_verified": 21, "account_in_review": 22, "account_approved": 23, "account_active": 24, "account_paused": 25, "account_restricted": 26, "account_suspended": 27, "account_banned": 28, "account_resigned": 29, "account_archived": 30, "email_outdated": 31, "identity_unverified": 32, "identity_verified": 33, "identity_in_review": 34, "identity_archived": 35, "identity_rejected": 36, "billing_subscription_pending": 1, "billing_subscription_active": 2, "billing_subscription_cancelled": 3, "billing_subscription_adhoc_charged": 4, "ledger_payout_pending": 1, "ledger_payout_processing": 2, "ledger_payout_processed": 3, "ledger_payout_rejected": 4, "ledger_entry_pending": 1, "ledger_entry_processing": 2, "ledger_entry_processed": 3, "billing_plan_active": 1, "billing_plan_draft": 2, "billing_plan_archived": 3, "post_draft": 1, "post_published": 2, "post_archived": 3, "product_draft": 1, "product_published": 2, "product_archived": 3, "legal_request_pending": 1, "legal_request_in_review": 2, "legal_request_approved": 3, "legal_request_rejected": 4, "financial_request_pending": 1, "financial_request_in_review": 2, "financial_request_approved": 3, "financial_request_rejected": 4 } }; // Translation utilities for converting between string representations and integer values // Create reverse mappings for integer to string conversion const createReverseMapping = (mapping) => { const reversed = {}; for (const [key, value] of Object.entries(mapping)) { reversed[value] = key; } return reversed; }; const reverseFeeStructure = createReverseMapping(mappings.FeeStructure); const reverseKind = createReverseMapping(mappings.Kind); const reverseStatus = createReverseMapping(mappings.Status); createReverseMapping(mappings.Access); // Helper to find a key by value and prefix, useful when multiple keys map to the same value const findKeyByValueAndPrefix = (mapping, value, prefix) => { for (const [key, val] of Object.entries(mapping)) { if (val === value && key.startsWith(prefix)) { return key; } } return undefined; }; /** * Translation functions for Fee Structures */ const FeeStructureTranslator = { /** * Convert string to integer for API calls */ toInteger(key) { return mappings.FeeStructure[key]; }, /** * Convert integer to string for user display */ toString(value) { const key = reverseFeeStructure[value]; if (!key) { throw new Error(`Unknown fee structure value: ${value}`); } return key; }, /** * Get all available options as string keys */ getOptions() { return Object.keys(mappings.FeeStructure); } }; /** * Translation functions for Kinds with context-aware prefixing */ const KindTranslator = { /** * Convert string to integer for API calls */ toInteger(key) { return mappings.Kind[key]; }, /** * Convert string to integer with context prefix */ toIntegerWithContext(key, context) { // If key already has a context prefix, use as-is const fullKey = key.includes('_') ? key : `${context}_${key}`; if (mappings.Kind[fullKey] !== undefined) { return mappings.Kind[fullKey]; } // Fallback: try the key as-is if it's a valid kind if (mappings.Kind[key] !== undefined) { return mappings.Kind[key]; } throw new Error(`Unknown kind value: ${key} (tried with context: ${fullKey})`); }, /** * Convert integer to string for user display */ toString(value) { const key = reverseKind[value]; if (!key) { throw new Error(`Unknown kind value: ${value}`); } return key; }, /** * Convert integer to string and remove context prefix */ toStringWithoutContext(value, context) { const prefix = `${context}_`; // Try to find a key that matches the value and starts with the prefix const contextKey = findKeyByValueAndPrefix(mappings.Kind, value, prefix); if (contextKey) { return contextKey.substring(prefix.length); } // Fallback to the global reverse mapping if no context-specific key is found const fullKey = this.toString(value); if (fullKey.startsWith(prefix)) { return fullKey.substring(prefix.length); } return fullKey; }, /** * Get all available options as string keys */ getOptions() { return Object.keys(mappings.Kind); }, /** * Get options filtered by prefix (e.g., 'order_', 'product_') */ getOptionsByPrefix(prefix) { return this.getOptions().filter(key => key.startsWith(prefix)); }, /** * Get options without context prefix for a specific context */ getContextualOptions(context) { const prefix = `${context}_`; return this.getOptions() .filter(key => key.startsWith(prefix)) .map(key => key.substring(prefix.length)); } }; /** * Translation functions for Statuses with context-aware prefixing */ const StatusTranslator = { /** * Convert string to integer for API calls */ toInteger(key) { return mappings.Status[key]; }, /** * Convert string to integer with context prefix */ toIntegerWithContext(key, context) { // If key already has the context prefix, use as-is const fullKey = key.includes('_') ? key : `${context}_${key}`; if (mappings.Status[fullKey] !== undefined) { return mappings.Status[fullKey]; } // Fallback: try the key as-is if it's a valid status if (mappings.Status[key] !== undefined) { return mappings.Status[key]; } throw new Error(`Unknown status value: ${key} (tried with context: ${fullKey})`); }, /** * Convert integer to string for user display */ toString(value) { const key = reverseStatus[value]; if (!key) { throw new Error(`Unknown status value: ${value}`); } return key; }, /** * Convert integer to string and remove context prefix */ toStringWithoutContext(value, context) { const prefix = `${context}_`; // Try to find a key that matches the value and starts with the prefix const contextKey = findKeyByValueAndPrefix(mappings.Status, value, prefix); if (contextKey) { return contextKey.substring(prefix.length); } // Fallback to the global reverse mapping if no context-specific key is found const fullKey = this.toString(value); if (fullKey.startsWith(prefix)) { return fullKey.substring(prefix.length); } return fullKey; }, /** * Get all available options as string keys */ getOptions() { return Object.keys(mappings.Status); }, /** * Get options filtered by prefix (e.g., 'order_', 'product_', 'account_') */ getOptionsByPrefix(prefix) { return this.getOptions().filter(key => key.startsWith(prefix)); }, /** * Get options without context prefix for a specific context */ getContextualOptions(context) { const prefix = `${context}_`; return this.getOptions() .filter(key => key.startsWith(prefix)) .map(key => key.substring(prefix.length)); } }; /** * Type-Based Query System * * This module provides a clean, type-safe query API where users write intuitive queries * and the SDK automatically transforms them into the Elixir-compatible format. * * Features: * - Array values → _in suffix (id: [1,2,3] → id_in: [1,2,3]) * - Range objects → _min/_max suffixes (age: {min: 18, max: 65} → age_min: 18, age_max: 65) * - String operations → contains. prefix (name: {contains: "john"} → "contains.name": "john") * - Date operations → before./after./on. prefixes * - JSON field operations → in_, not_, null_, not_null_ prefixes * - Direct values → equality check (no transformation) */ /** * Runtime validation for query parameters */ function validateQueryParams(query, fieldTypes) { const errors = []; if (!query || typeof query !== 'object') { return errors; } for (const [key, value] of Object.entries(query)) { // Skip special fields and undefined/null values if (isSpecialField(key) || value === undefined || value === null) { continue; } // Skip data field (JSON queries have their own validation) if (key === 'data') { continue; } const fieldType = fieldTypes === null || fieldTypes === void 0 ? void 0 : fieldTypes[key]; // Validate based on field type if (fieldType) { const validationError = validateFieldValue(key, value, fieldType); if (validationError) { errors.push(validationError); } } } return errors; } /** * Validate a single field value against its expected type */ function validateFieldValue(fieldName, value, expectedType) { // Handle array values (for _in operations) if (Array.isArray(value)) { for (const item of value) { if (!isValueOfType(item, expectedType)) { return `Field "${fieldName}" array contains invalid type. Expected all items to be ${expectedType}, but found ${typeof item}`; } } return null; } // Handle range objects if (typeof value === 'object' && value !== null && ('min' in value || 'max' in value)) { if (expectedType !== 'number' && expectedType !== 'date' && expectedType !== 'string') { return `Field "${fieldName}" cannot use range queries. Range queries are only supported for number, date, and string fields.`; } if ('min' in value && value.min !== undefined && !isValueOfType(value.min, expectedType)) { return `Field "${fieldName}" range min value has wrong type. Expected ${expectedType}, got ${typeof value.min}`; } if ('max' in value && value.max !== undefined && !isValueOfType(value.max, expectedType)) { return `Field "${fieldName}" range max value has wrong type. Expected ${expectedType}, got ${typeof value.max}`; } return null; } // Handle string contains queries if (typeof value === 'object' && value !== null && 'contains' in value) { if (expectedType !== 'string') { return `Field "${fieldName}" cannot use contains queries. Contains queries are only supported for string fields.`; } if (typeof value.contains !== 'string') { return `Field "${fieldName}" contains value must be a string. Got ${typeof value.contains}`; } return null; } // Handle date queries if (typeof value === 'object' && value !== null && ('before' in value || 'after' in value || 'on' in value || 'min' in value || 'max' in value)) { if ('before' in value && value.before !== undefined && typeof value.before !== 'string') { return `Field "${fieldName}" before value must be a string. Got ${typeof value.before}`; } if ('after' in value && value.after !== undefined && typeof value.after !== 'string') { return `Field "${fieldName}" after value must be a string. Got ${typeof value.after}`; } if ('on' in value && value.on !== undefined && typeof value.on !== 'string') { return `Field "${fieldName}" on value must be a string. Got ${typeof value.on}`; } if ('min' in value && value.min !== undefined && typeof value.min !== 'string') { return `Field "${fieldName}" min value must be a string. Got ${typeof value.min}`; } if ('max' in value && value.max !== undefined && typeof value.max !== 'string') { return `Field "${fieldName}" max value must be a string. Got ${typeof value.max}`; } return null; } // Handle direct values if (!isValueOfType(value, expectedType)) { return `Field "${fieldName}" has wrong type. Expected ${expectedType}, got ${typeof value}`; } return null; } /** * Check if a value matches the expected type */ function isValueOfType(value, expectedType) { switch (expectedType) { case 'string': return typeof value === 'string'; case 'number': return typeof value === 'number' && !isNaN(value); case 'boolean': return typeof value === 'boolean'; case 'date': return value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value))); case 'array': return Array.isArray(value); default: return true; } } /** * Transform a clean user query into Elixir-compatible format */ function transformQuery(query) { if (!query || typeof query !== 'object') { return {}; } const result = {}; for (const [key, value] of Object.entries(query)) { // Skip undefined/null values if (value === undefined || value === null) { continue; } // Pass through special fields unchanged if (isSpecialField(key)) { result[key] = value; continue; } // Handle data field specially for JSON queries if (key === 'data' && typeof value === 'object') { result.data = transformJsonQuery(value); continue; } // Transform based on value type const transformedValue = transformFieldValue(key, value); // Skip null values (e.g., from empty objects) if (transformedValue !== null) { result[key] = transformedValue; } } return result; } /** * Check if a field is a special field that should pass through unchanged */ function isSpecialField(key) { const specialFields = [ 'exclude', 'distinct', 'order_by', 'page', 'page_size', 'per_page', 'limit', 'override_page', 'q', 'search', 'sort', 'order' ]; return specialFields.includes(key); } /** * Transform a field value based on its type */ function transformFieldValue(key, value) { if (Array.isArray(value)) { // Array → add _in suffix return { [`${key}_in`]: value }; } if (typeof value === 'object' && value !== null) { const transformedObject = {}; // Handle range queries (min/max) if ('min' in value && value.min !== undefined) { transformedObject[`${key}_min`] = value.min; } if ('max' in value && value.max !== undefined) { transformedObject[`${key}_max`] = value.max; } // Handle range queries (gte/lte/gt/lt) if ('gte' in value && value.gte !== undefined) { transformedObject[`${key}_gte`] = value.gte; } if ('lte' in value && value.lte !== undefined) { transformedObject[`${key}_lte`] = value.lte; } if ('gt' in value && value.gt !== undefined) { transformedObject[`${key}_gt`] = value.gt; } if ('lt' in value && value.lt !== undefined) { transformedObject[`${key}_lt`] = value.lt; } // Handle string queries if ('contains' in value && value.contains !== undefined) { transformedObject[`contains.${key}`] = value.contains; } // Handle date queries if ('before' in value && value.before !== undefined) { transformedObject[`before.${key}`] = value.before; } if ('after' in value && value.after !== undefined) { transformedObject[`after.${key}`] = value.after; } if ('on' in value && value.on !== undefined) { transformedObject[`on.${key}`] = value.on; } // If we found any transformations, return them if (Object.keys(transformedObject).length > 0) { return transformedObject; } // Empty object with no transformation keys - return null to skip it if (Object.keys(value).length === 0) { return null; } } // Direct value → wrap for consistent structure that will be flattened later return { [key]: value }; } /** * Transform JSON field queries with special operators */ function transformJsonQuery(data) { const result = {}; for (const [key, value] of Object.entries(data)) { // Skip undefined/null values if (value === undefined || value === null) { continue; } // Nested paths (e.g., "settings->theme") stay as-is if (key.includes('->')) { result[key] = value; continue; } if (typeof value === 'object' && value !== null) { // Check if this is a JSON query operation (has special keys) const hasJsonQueryOps = 'in' in value || 'not' in value || 'null' in value || 'not_null' in value || 'min' in value || 'max' in value; if (hasJsonQueryOps) { // Transform JSON-specific operations if ('in' in value && value.in !== undefined) { result[`in_${key}`] = value.in; } if ('not' in value && value.not !== undefined) { result[`not_${key}`] = value.not; } if ('null' in value && value.null !== undefined) { result[`null_${key}`] = value.null; } if ('not_null' in value && value.not_null !== undefined) { result[`not_null_${key}`] = value.not_null; } if ('min' in value && value.min !== undefined) { result[`${key}_min`] = value.min; } if ('max' in value && value.max !== undefined) { result[`${key}_max`] = value.max; } } else { // Complex object without query operators - pass through as-is result[key] = value; } } else { // Direct value in JSON field result[key] = value; } } return result; } /** * Flatten the transformed query object for API consumption */ function flattenTransformedQuery(transformed) { const result = {}; for (const [key, value] of Object.entries(transformed)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // If it's a transformation object with special keys, merge its properties if (hasTransformationKeys(value)) { Object.assign(result, value); } else if (isWrappedDirectValue(key, value)) { // Unwrap direct values like { id: { id: 5 } } → { id: 5 } result[key] = value[key]; } else { // Regular object (like data field) result[key] = value; } } else { // Direct value or array result[key] = value; } } return result; } /** * Check if a value is a wrapped direct value (e.g., { id: { id: 5 } }) * This happens when transformFieldValue wraps a direct value for consistency */ function isWrappedDirectValue(key, obj) { const keys = Object.keys(obj); return keys.length === 1 && keys[0] === key; } /** * Check if an object contains transformation keys */ function hasTransformationKeys(obj) { const keys = Object.keys(obj); return keys.some(key => key.includes('_min') || key.includes('_max') || key.includes('_gte') || key.includes('_lte') || key.includes('_gt') || key.includes('_lt') || key.includes('_in') || key.includes('contains.') || key.includes('before.') || key.includes('after.') || key.includes('on.')); } /** * Main function to transform and flatten a query in one step * Handles translation of contextual strings to integers before transformation */ function processQuery(query, fieldTypes, options = { validate: false }) { // Translate contextual strings to integers BEFORE validation and transformation const translatedQuery = { ...query }; if (fieldTypes && options.context) { for (const [key, value] of Object.entries(translatedQuery)) { const fieldType = fieldTypes[key]; // Skip special fields if (isSpecialField(key)) continue; // Translate status fields (contextual strings to integers) if (key === 'status' && fieldType === 'number') { translatedQuery[key] = translateValue(value, StatusTranslator, options.context); } // Translate kind fields (contextual strings to integers) if (key === 'kind' && fieldType === 'number') { translatedQuery[key] = translateValue(value, KindTranslator, options.context); } } } // Transform AFTER translation so that range objects are properly handled const transformed = transformQuery(translatedQuery); const flattened = flattenTransformedQuery(transformed); // Runtime validation AFTER transformation if enabled and field types provided if (options.validate && fieldTypes) { const validationErrors = validateQueryParams(flattened, fieldTypes); if (validationErrors.length > 0) { console.warn(`Query validation warnings: ${validationErrors.join(', ')}`); } } return flattened; } /** * Helper to translate a value (string, array of strings, or object with strings) */ function translateValue(value, translator, context) { if (value === undefined || value === null) { return value; } // Handle arrays (for _in operations) if (Array.isArray(value)) { return value.map(item => { // If it's already a number, pass it through if (typeof item === 'number') { return item; } // If it's a string, it MUST be translatable if (typeof item === 'string') { return context ? translator.toIntegerWithContext(item, context) : translator.toInteger(item); } return item; }); } // Handle range objects (e.g., { gte: 'paid', lte: 'confirmed' }) if (typeof value === 'object' && value !== null) { const translated = {}; for (const [k, v] of Object.entries(value)) { // If it's already a number, pass it through if (typeof v === 'number') { translated[k] = v; } // If it's a string, it MUST be translatable else if (typeof v === 'string') { translated[k] = context ? translator.toIntegerWithContext(v, context) : translator.toInteger(v); } else { translated[k] = v; } } return translated; } // Handle direct string values if (typeof value === 'string') { return context ? translator.toIntegerWithContext(value, context) : translator.toInteger(value); } // Already a number, pass through return value; } /** * Type-safe query builder for specific entity types */ class QueryBuilder { constructor(initialQuery) { this.query = {}; if (initialQuery) { this.query = { ...initialQuery }; } } /** * Add a field equality condition */ where(field, value) { this.query[field] = value; return this; } /** * Add a field IN condition (array of values) */ whereIn(field, values) { this.query[field] = values; return this; } /** * Add a range condition (min/max) */ whereRange(field, min, max) { const range = {}; if (min !== undefined) range.min = min; if (max !== undefined) range.max = max; this.query[field] = range; return this; } /** * Add a string contains condition */ whereContains(field, value) { this.query[field] = { contains: value }; return this; } /** * Add a date range condition */ whereDateRange(field, after, before, on) { const dateQuery = {}; if (after !== undefined) dateQuery.after = after; if (before !== undefined) dateQuery.before = before; if (on !== undefined) dateQuery.on = on; this.query[field] = dateQuery; return this; } /** * Add pagination */ paginate(page, pageSize) { this.query.page = page; this.query.page_size = pageSize; return this; } /** * Add ordering */ orderBy(field, direction = 'asc') { this.query.order_by = `${field} ${direction}`; return this; } /** * Add general search */ search(term) { this.query.q = term; return this; } /** * Build and return the transformed query */ build() { return processQuery(this.query); } /** * Get the raw query (before transformation) */ getRawQuery() { return { ...this.query }; } } /** * Resource-specific query builders * * This file provides fluent query builder interfaces for each resource type, * offering excellent IntelliSense and type safety for complex queries. */ /** * Order Query Builder * Provides a fluent interface for building complex order queries * * @example * const orders = await sdk.orders.createQueryBuilder() * .whereStatus('confirmed') * .whereTotalRange(100, 1000) * .whereReferenceContains('ORDER-2024') * .paginate(1, 20) * .orderBy('inserted_at', 'desc') * .execute(); */ class OrderQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by order status (contextual values) */ whereStatus(status) { if (Array.isArray(status)) { return this.whereIn('status', status); } return this.where('status', status); } /** * Filter by order kind/type (contextual values) */ whereKind(kind) { if (Array.isArray(kind)) { return this.whereIn('kind', kind); } return this.where('kind', kind); } /** * Filter by total amount range */ whereTotalRange(min, max) { return this.whereRange('total', min, max); } /** * Filter by reference ID containing a string */ whereReferenceContains(value) { return this.whereContains('reference_id', value); } /** * Filter by creation date range */ whereCreatedBetween(after, before) { return this.whereDateRange('inserted_at', after, before); } /** * Filter by customer ID */ whereCustomer(customerId) { if (Array.isArray(customerId)) { return this.whereIn('customer_id', customerId); } return this.where('customer_id', customerId); } /** * Filter by billing plan ID */ whereBillingPlan(planId) { if (Array.isArray(planId)) { return this.whereIn('billing_plan_id', planId); } return this.where('billing_plan_id', planId); } } /** * Product Query Builder * Provides a fluent interface for building complex product queries * * @example * const products = await sdk.products.createQueryBuilder() * .whereStatus('published') * .wherePriceRange(10, 100) * .whereTitleContains('shirt') * .wherePublic(true) * .paginate(1, 20) * .execute(); */ class ProductQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by product status */ whereStatus(status) { if (Array.isArray(status)) { return this.whereIn('status', status); } return this.where('status', status); } /** * Filter by price range */ wherePriceRange(min, max) { return this.whereRange('price', min, max); } /** * Filter by title containing a string */ whereTitleContains(value) { return this.whereContains('title', value); } /** * Filter by public visibility */ wherePublic(isPublic) { return this.where('public', isPublic); } /** * Filter by category */ whereCategory(categoryId) { if (Array.isArray(categoryId)) { return this.whereIn('category_id', categoryId); } return this.where('category_id', categoryId); } /** * Filter by availability (units remaining) */ whereUnitsRemainingRange(min, max) { return this.whereRange('units_remaining', min, max); } /** * Filter by unlimited flag */ whereUnlimited(isUnlimited) { return this.where('unlimited', isUnlimited); } } /** * User Query Builder * Provides a fluent interface for building complex user queries * * @example * const users = await sdk.users.createQueryBuilder() * .whereStatus('approved') * .whereKind('organisation') * .whereEmailContains('@example.com') * .whereLevelRange(5, 10) * .paginate(1, 20) * .execute(); */ class UserQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by account status */ whereStatus(status) { if (Array.isArray(status)) { return this.whereIn('status', status); } return this.where('status', status); } /** * Filter by user kind/type */ whereKind(kind) { if (Array.isArray(kind)) { return this.whereIn('kind', kind); } return this.where('kind', kind); } /** * Filter by email containing a string */ whereEmailContains(value) { return this.whereContains('email', value); } /** * Filter by username containing a string */ whereUsernameContains(value) { return this.whereContains('username', value); } /** * Filter by user level range */ whereLevelRange(min, max) { return this.whereRange('level', min, max); } /** * Filter by organization */ whereOrganisation(orgId) { if (Array.isArray(orgId)) { return this.whereIn('organisation_id', orgId); } return this.where('organisation_id', orgId); } /** * Filter by role */ whereRole(roleId) { if (Array.isArray(roleId)) { return this.whereIn('role_id', roleId); } return this.where('role_id', roleId); } } /** * Merchant Query Builder * Provides a fluent interface for building complex merchant queries * * @example * const merchants = await sdk.merchants.createQueryBuilder() * .whereStatus('approved') * .whereNameContains('Store') * .whereSector('retail') * .paginate(1, 20) * .execute(); */ class MerchantQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by merchant status */ whereStatus(status) { if (Array.isArray(status)) { return this.whereIn('status', status); } return this.where('status', status); } /** * Filter by name containing a string */ whereNameContains(value) { return this.whereContains('name', value); } /** * Filter by email containing a string */ whereEmailContains(value) { return this.whereContains('email', value); } /** * Filter by sector */ whereSector(sector) { if (Array.isArray(sector)) { return this.whereIn('sector', sector); } return this.where('sector', sector); } /** * Filter by business type */ whereBusinessType(type) { if (Array.isArray(type)) { return this.whereIn('business_type', type); } return this.where('business_type', type); } /** * Filter by platform fee structure */ wherePlatformFeeStructure(structure) { if (Array.isArray(structure)) { return this.whereIn('platform_fee_structure', structure); } return this.where('platform_fee_structure', structure); } /** * Filter by organisation */ whereOrganisation(orgId) { if (Array.isArray(orgId)) { return this.whereIn('organisation_id', orgId); } return this.where('organisation_id', orgId); } } /** * Category Query Builder * Provides a fluent interface for building complex category queries * * @example * const categories = await sdk.categories.createQueryBuilder() * .whereKind('published') * .whereNameContains('Electronics') * .whereParent(null) * .paginate(1, 20) * .execute(); */ class CategoryQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by category kind */ whereKind(kind) { if (Array.isArray(kind)) { return this.whereIn('kind', kind); } return this.where('kind', kind); } /** * Filter by name containing a string */ whereNameContains(value) { return this.whereContains('name', value); } /** * Filter by parent category */ whereParent(parentId) { if (parentId === null) { return this.where('parent_id', null); } if (Array.isArray(parentId)) { return this.whereIn('parent_id', parentId); } return this.where('parent_id', parentId); } /** * Filter by root categories only (no parent) */ whereRootOnly() { return this.where('parent_id', null); } } /** * Billing Plan Query Builder * Provides a fluent interface for building complex billing plan queries * * @example * const plans = await sdk.billingPlans.createQueryBuilder() * .whereKind('subscription') * .wherePriceRange(10, 100) * .wherePublic(true) * .paginate(1, 20) * .execute(); */ class BillingPlanQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by plan kind/type */ whereKind(kind) { if (Array.isArray(kind)) { return this.whereIn('kind', kind); } return this.where('kind', kind); } /** * Filter by flat rate range */ whereFlatRateRange(min, max) { return this.whereRange('flat_rate', min, max); } /** * Filter by transaction fee range */ whereTransactionFeeRange(min, max) { return this.whereRange('transaction_fee', min, max); } /** * Filter by public visibility */ wherePublic(isPublic) { return this.where('public', isPublic); } /** * Filter by auto charge */ whereAutoCharge(autoCharge) { return this.where('auto_charge', autoCharge); } /** * Filter by name containing a string */ whereNameContains(value) { return this.whereContains('name', value); } /** * Filter by duration range (in days) */ whereDurationRange(min, max) { return this.whereRange('duration', min, max); } } /** * Subscription Query Builder * Provides a fluent interface for building complex subscription queries * * @example * const subscriptions = await sdk.subscriptions.createQueryBuilder() * .whereStatus('active') * .whereBillingPlan(123) * .whereStartDateAfter('2024-01-01') * .paginate(1, 20) * .execute(); */ class SubscriptionQueryBuilder extends QueryBuilder { constructor(resource, initialQuery) { super(initialQuery); this.resource = resource; } /** * Execute the query and return the results */ async execute() { return this.resource.query(this.getRawQuery()); } /** * Filter by subscription status */ whereStatus(status) { if (Array.isArray(status)) { return this.whereIn('status', status); } return this.where('status', status); } /** * Filter by billing plan */ whereBillingPlan(planId) { if (Array.isArray(planId)) { return this.whereIn('billing_plan_id', planId); } return this.where('billing_plan_id', planId); } /** * Filter by customer */ whereCustomer(customerId) { if (Array.isArray(customerId)) { return this.whereIn('customer_id', customerId); } return this.where('customer_id', customerId); } /** * Filter by start date after a specific date */ whereStartDateAfter(date) { return this.whereDateRange('start_date', date, undefined); } /** * Filter by start date before a specific date */ whereStartDateBefore(date) { return this.whereDateRange('start_date', undefined, date); } /** * Filter by active subscriptions (not canceled) */ whereActive() { return this.where('canceled_at', null); } /** * Filter by canceled subscriptions */ whereCanceled() { // This would need a "not null" check which might require additional logic // For now, we can use a date range that's been filled return this.whereDateRange('canceled_at', '1970-01-01', undefined); } } /** * Payment Link Query Builder */ class PaymentLinkQueryBuilder extends QueryBuilder { constructor(resource, i