Skip to main content
Glama
tierService.js10.1 kB
import { ValidationError } from '../errors/index.js'; /** * Maximum length constants (following Ghost's database constraints) */ const MAX_NAME_LENGTH = 191; // Ghost's typical varchar limit const MAX_DESCRIPTION_LENGTH = 2000; // Reasonable limit for descriptions /** * Query constraints for tier browsing */ const MIN_LIMIT = 1; const MAX_LIMIT = 100; const MIN_PAGE = 1; /** * Currency code validation regex (3-letter uppercase) */ const CURRENCY_REGEX = /^[A-Z]{3}$/; /** * URL validation regex (simple HTTP/HTTPS validation) */ const URL_REGEX = /^https?:\/\/.+/i; /** * Validates tier data for creation * @param {Object} tierData - The tier data to validate * @throws {ValidationError} If validation fails */ export function validateTierData(tierData) { const errors = []; // Name is required and must be a non-empty string if (!tierData.name || typeof tierData.name !== 'string' || tierData.name.trim().length === 0) { errors.push({ field: 'name', message: 'Name is required and must be a non-empty string' }); } else if (tierData.name.length > MAX_NAME_LENGTH) { errors.push({ field: 'name', message: `Name must not exceed ${MAX_NAME_LENGTH} characters` }); } // Currency is required and must be a 3-letter uppercase code if (!tierData.currency || typeof tierData.currency !== 'string') { errors.push({ field: 'currency', message: 'Currency is required' }); } else if (!CURRENCY_REGEX.test(tierData.currency)) { errors.push({ field: 'currency', message: 'Currency must be a 3-letter uppercase code (e.g., USD, EUR)', }); } // Description is optional but must be a string with valid length if provided if (tierData.description !== undefined) { if (typeof tierData.description !== 'string') { errors.push({ field: 'description', message: 'Description must be a string' }); } else if (tierData.description.length > MAX_DESCRIPTION_LENGTH) { errors.push({ field: 'description', message: `Description must not exceed ${MAX_DESCRIPTION_LENGTH} characters`, }); } } // Monthly price is optional but must be a non-negative number if provided if (tierData.monthly_price !== undefined) { if (typeof tierData.monthly_price !== 'number' || tierData.monthly_price < 0) { errors.push({ field: 'monthly_price', message: 'Monthly price must be a non-negative number', }); } } // Yearly price is optional but must be a non-negative number if provided if (tierData.yearly_price !== undefined) { if (typeof tierData.yearly_price !== 'number' || tierData.yearly_price < 0) { errors.push({ field: 'yearly_price', message: 'Yearly price must be a non-negative number', }); } } // Benefits is optional but must be an array of non-empty strings if provided if (tierData.benefits !== undefined) { if (!Array.isArray(tierData.benefits)) { errors.push({ field: 'benefits', message: 'Benefits must be an array' }); } else { // Validate each benefit is a non-empty string const invalidBenefits = tierData.benefits.filter( (benefit) => typeof benefit !== 'string' || benefit.trim().length === 0 ); if (invalidBenefits.length > 0) { errors.push({ field: 'benefits', message: 'Each benefit must be a non-empty string', }); } } } // Welcome page URL is optional but must be a valid HTTP/HTTPS URL if provided if (tierData.welcome_page_url !== undefined) { if ( typeof tierData.welcome_page_url !== 'string' || !URL_REGEX.test(tierData.welcome_page_url) ) { errors.push({ field: 'welcome_page_url', message: 'Welcome page URL must be a valid URL', }); } } if (errors.length > 0) { throw new ValidationError('Tier validation failed', errors); } } /** * Validates tier data for update * All fields are optional for updates, but if provided they must be valid * @param {Object} updateData - The tier update data to validate * @throws {ValidationError} If validation fails */ export function validateTierUpdateData(updateData) { const errors = []; // Name is optional for update but must be a non-empty string with valid length if provided if (updateData.name !== undefined) { if (typeof updateData.name !== 'string' || updateData.name.trim().length === 0) { errors.push({ field: 'name', message: 'Name must be a non-empty string' }); } else if (updateData.name.length > MAX_NAME_LENGTH) { errors.push({ field: 'name', message: `Name must not exceed ${MAX_NAME_LENGTH} characters` }); } } // Currency is optional for update but must be a 3-letter uppercase code if provided if (updateData.currency !== undefined) { if (typeof updateData.currency !== 'string' || !CURRENCY_REGEX.test(updateData.currency)) { errors.push({ field: 'currency', message: 'Currency must be a 3-letter uppercase code (e.g., USD, EUR)', }); } } // Description is optional but must be a string with valid length if provided if (updateData.description !== undefined) { if (typeof updateData.description !== 'string') { errors.push({ field: 'description', message: 'Description must be a string' }); } else if (updateData.description.length > MAX_DESCRIPTION_LENGTH) { errors.push({ field: 'description', message: `Description must not exceed ${MAX_DESCRIPTION_LENGTH} characters`, }); } } // Monthly price is optional but must be a non-negative number if provided if (updateData.monthly_price !== undefined) { if (typeof updateData.monthly_price !== 'number' || updateData.monthly_price < 0) { errors.push({ field: 'monthly_price', message: 'Monthly price must be a non-negative number', }); } } // Yearly price is optional but must be a non-negative number if provided if (updateData.yearly_price !== undefined) { if (typeof updateData.yearly_price !== 'number' || updateData.yearly_price < 0) { errors.push({ field: 'yearly_price', message: 'Yearly price must be a non-negative number', }); } } // Benefits is optional but must be an array of non-empty strings if provided if (updateData.benefits !== undefined) { if (!Array.isArray(updateData.benefits)) { errors.push({ field: 'benefits', message: 'Benefits must be an array' }); } else { // Validate each benefit is a non-empty string const invalidBenefits = updateData.benefits.filter( (benefit) => typeof benefit !== 'string' || benefit.trim().length === 0 ); if (invalidBenefits.length > 0) { errors.push({ field: 'benefits', message: 'Each benefit must be a non-empty string', }); } } } // Welcome page URL is optional but must be a valid HTTP/HTTPS URL if provided if (updateData.welcome_page_url !== undefined) { if ( typeof updateData.welcome_page_url !== 'string' || !URL_REGEX.test(updateData.welcome_page_url) ) { errors.push({ field: 'welcome_page_url', message: 'Welcome page URL must be a valid URL', }); } } if (errors.length > 0) { throw new ValidationError('Tier validation failed', errors); } } /** * Validates query options for tier browsing * @param {Object} options - The query options to validate * @param {number} [options.limit] - Number of tiers to return (1-100) * @param {number} [options.page] - Page number (1+) * @param {string} [options.filter] - NQL filter string * @param {string} [options.order] - Order string (e.g., 'created_at desc') * @param {string} [options.include] - Include string (e.g., 'monthly_price,yearly_price') * @throws {ValidationError} If validation fails */ export function validateTierQueryOptions(options) { const errors = []; // Validate limit if (options.limit !== undefined) { if ( typeof options.limit !== 'number' || options.limit < MIN_LIMIT || options.limit > MAX_LIMIT ) { errors.push({ field: 'limit', message: `Limit must be a number between ${MIN_LIMIT} and ${MAX_LIMIT}`, }); } } // Validate page if (options.page !== undefined) { if (typeof options.page !== 'number' || options.page < MIN_PAGE) { errors.push({ field: 'page', message: `Page must be a number >= ${MIN_PAGE}`, }); } } // Validate filter (must be non-empty string if provided) if (options.filter !== undefined) { if (typeof options.filter !== 'string' || options.filter.trim().length === 0) { errors.push({ field: 'filter', message: 'Filter must be a non-empty string', }); } } // Validate order (must be non-empty string if provided) if (options.order !== undefined) { if (typeof options.order !== 'string' || options.order.trim().length === 0) { errors.push({ field: 'order', message: 'Order must be a non-empty string', }); } } // Validate include (must be non-empty string if provided) if (options.include !== undefined) { if (typeof options.include !== 'string' || options.include.trim().length === 0) { errors.push({ field: 'include', message: 'Include must be a non-empty string', }); } } if (errors.length > 0) { throw new ValidationError('Tier query validation failed', errors); } } /** * Sanitizes a value for use in NQL filters to prevent injection * Escapes backslashes, single quotes, and double quotes * @param {string} value - The value to sanitize * @returns {string} The sanitized value */ export function sanitizeNqlValue(value) { if (!value) return value; // Escape backslashes first, then quotes return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); } export default { validateTierData, validateTierUpdateData, validateTierQueryOptions, sanitizeNqlValue, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jgardner04/Ghost-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server