@inkress/admin-sdk
Version:
Official Inkress Commerce API SDK for JavaScript/TypeScript
1,574 lines (1,568 loc) • 141 kB
JavaScript
import fetch from '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, initialQuery) {
super(initialQuery);
this.resource = resource;