pricing4ts
Version:
 Pricing4TS is a TypeScript-based toolkit designed to enhance the server-side functionality of a pricing-driven SaaS by enabling the seamless integration of pricing plans into the application logic. T
1,017 lines (847 loc) • 33.5 kB
text/typescript
import type { AddOn } from '../models/pricing2yaml/addon';
import type { AutomationType, Feature, IntegrationType, PaymentType } from '../../types';
import type { Plan } from '../models/pricing2yaml/plan';
import type { Pricing } from '../models/pricing2yaml/pricing';
import type {
FeatureType,
RenderMode,
UsageLimitType,
ValueType,
} from '../models/pricing2yaml/types';
import type { ContainerUsageLimits, UsageLimit } from '../models/pricing2yaml/usage-limit';
import * as cc from 'currency-codes';
import { isAutomationType, isIntegrationType } from '../models/pricing2yaml/feature';
const VERSION_REGEXP = /^\d+\.\d+$/;
const unlimitedValue = 100000000;
export function validateName(name: string | null, item: string): string {
if (name === null || name === undefined) {
throw new Error(`The ${item} must have a name`);
}
if (typeof name !== 'string') {
throw new TypeError(`The ${item} name must be a string`);
}
const trimmedName = name.trim();
if (trimmedName.length === 0) {
throw new Error(`The ${item} name must not be empty`);
}
if (trimmedName.length < 3) {
throw new Error(`The ${item} name must have at least 3 characters`);
}
if (trimmedName.length > 255) {
throw new Error(`The ${item} name must have at most 255 characters`);
}
return trimmedName;
}
export function validateSyntaxVersion(version: string): string {
if (version === null || version === undefined) {
throw new Error(
`The syntaxVersion field of the pricing must not be null or undefined. Please ensure that the syntaxVersion field is present and correctly formatted`
);
}
if (typeof version !== 'string' || !VERSION_REGEXP.test(version)) {
throw new TypeError(
`The syntaxVersion field of the pricing does not follow the required structure: X.Y (being X and Y numbers). Please ensure it is a string in the format X.Y`
);
}
return version;
}
export function validateVersion(version: string | undefined, createdAt: Date): string {
version ??= `${createdAt.getFullYear()}-${createdAt.getMonth() + 1}-${createdAt.getDate()}`;
if (typeof version !== 'string') {
throw new TypeError(
`The version field of the pricing must be a string. Please ensure that the version field is present and correctly formatted`
);
}
return version;
}
export function validateCreatedAt(createdAt: string | Date | null): Date {
if (createdAt === null || createdAt === undefined) {
throw new Error(
`The createdAt field must not be null or undefined. Please ensure that the createdAt field is present and correctly formatted (as Date or string)`
);
}
if (typeof createdAt === 'string'){
if (/^\d{4}-\d{2}-\d{2}$/.test(createdAt)) {
createdAt = new Date(createdAt);
} else {
throw new TypeError(
`The createdAt field must be a string in the format yyyy-mm-dd or a valid Date object`
);
}
}
if (!(createdAt instanceof Date) || isNaN(createdAt.getTime())) {
throw new TypeError(
`The createdAt field must be a valid Date object or a string in a recognized date format`
);
}
const now = new Date();
if (createdAt > now) {
throw new Error(`The createdAt field must not be a future date`);
}
return createdAt;
}
export function validateCurrency(currency: string | null): string {
if (currency === null || currency === undefined) {
throw new Error(
`The currency field of the pricing must not be null or undefined. Please ensure that the currency field is present and correctly formatted`
);
}
if (typeof currency !== 'string') {
throw new TypeError(`The currency field of the pricing must be a string`);
}
const trimmedCurrency = currency.trim();
if (trimmedCurrency.length === 0) {
throw new Error(`The currency field of the pricing must not be empty`);
}
const currencyCode = cc.code(trimmedCurrency);
if (!currencyCode) {
throw new Error(`The currency code ${trimmedCurrency} is not a valid ISO 4217 currency code`);
}
return currency;
}
export function validateDescription(description: string | null | undefined): string | undefined {
description ??= undefined;
return description;
}
export function validateValueType(valueType: string | null): ValueType {
if (valueType === null || valueType === undefined) {
throw new Error(
`The valueType field of a feature must not be null or undefined. Please ensure that the valueType field is present and it's value correspond to either BOOLEAN, NUMERIC or TEXT`
);
}
if (typeof valueType !== 'string') {
throw new TypeError(
`The valueType field of a feature must be a string, and its value must be either BOOLEAN, NUMERIC or TEXT. Received: ${valueType} `
);
}
valueType = valueType.trim().toUpperCase();
if (!['NUMERIC', 'BOOLEAN', 'TEXT'].includes(valueType)) {
throw new Error(
`The valueType field of a feature must be one of NUMERIC, BOOLEAN, or TEXT. Received: ${valueType}`
);
}
return valueType as ValueType;
}
export function validateDefaultValue(
elem: Feature | UsageLimit,
item: string
): number | boolean | string | PaymentType[] {
if (elem.defaultValue === null || elem.defaultValue === undefined) {
throw new Error(
`The defaultValue field of a ${item} must not be null or undefined. Please ensure that the defaultValue field is present and its defaultValue type correspond to the declared valueType`
);
}
switch (elem.valueType) {
case 'NUMERIC':
if (typeof elem.defaultValue !== 'number') {
throw new TypeError(
`The defaultValue field of a ${item} must be a number when its valueType is NUMERIC. Received: ${elem.defaultValue}`
);
}
if (elem.defaultValue > unlimitedValue){
elem.defaultValue = unlimitedValue;
}
break;
case 'BOOLEAN':
if (typeof elem.defaultValue !== 'boolean') {
throw new TypeError(
`The defaultValue field of a ${item} must be a boolean when its valueType is BOOLEAN. Received: ${elem.defaultValue}`
);
}
break;
case 'TEXT':
if (elem.type === 'PAYMENT') {
if (!(elem.defaultValue instanceof Array)) {
throw new TypeError(`Payment method value must be an array of payment methods`);
}
for (const paymentMethod of elem.defaultValue) {
if (
!['CARD', 'GATEWAY', 'INVOICE', 'ACH', 'WIRE_TRANSFER', 'OTHER'].includes(paymentMethod)
) {
throw new Error(
`Invalid payment method: ${paymentMethod}. Please provide one of the following: CARD, GATEWAY, INVOICE, ACH, WIRE_TRANSFER, OTHER`
);
}
}
break;
}
if (typeof elem.defaultValue !== 'string') {
throw new Error(
`The defaultValue field of a ${item} must be a string when its valueType is TEXT. Received: ${elem.defaultValue}`
);
}
break;
default:
throw new Error(
`The valueType field of a ${item} must be either BOOLEAN, NUMERIC or TEXT. Received: ${elem.valueType}`
);
}
return elem.defaultValue;
}
export function validateExpression(
expression: string | null | undefined,
item: string
): string | undefined {
expression ??= undefined;
if (typeof expression === 'string') {
if (expression.trim().length === 0) {
throw new Error(
`The ${item} field of a feature must not be empty. If you don't want to declare an expression for this feature, either use null or undefined`
);
}
}
return expression;
}
export function validateValue(
elem: Feature | UsageLimit,
item: string,
valueType: ValueType | undefined = undefined
): number | boolean | string | PaymentType[] | undefined {
elem.value ??= undefined;
const valueTypeToValidate = valueType || elem.valueType;
switch (valueTypeToValidate) {
case 'NUMERIC':
if (typeof elem.value !== 'number' && elem.value !== undefined) {
throw new TypeError(
`The value field of a ${item} must be a number when its valueType is NUMERIC. Received: ${elem.value}`
);
}
if (elem.value! > unlimitedValue){
elem.value = unlimitedValue;
}
break;
case 'BOOLEAN':
if (typeof elem.value !== 'boolean' && elem.value !== undefined) {
throw new TypeError(
`The value field of a ${item} must be a boolean when its valueType is BOOLEAN. Received: ${elem.value}`
);
}
break;
case 'TEXT':
if (elem.type === 'PAYMENT' && elem.value !== undefined) {
if (!(elem.value instanceof Array)) {
throw new TypeError(`Payment method value must be an array of payment methods`);
}
for (const paymentMethod of elem.value) {
if (
!['CARD', 'GATEWAY', 'INVOICE', 'ACH', 'WIRE_TRANSFER', 'OTHER'].includes(paymentMethod)
) {
throw new Error(
`Invalid payment method: ${paymentMethod}. Please provide one of the following: CARD, GATEWAY, INVOICE, ACH, WIRE_TRANSFER, OTHER`
);
}
}
break;
}
if (typeof elem.value !== 'string' && elem.value !== undefined) {
throw new TypeError(
`The value field of a ${item} must be a string when its valueType is TEXT. Received: ${elem.value}`
);
}
break;
default:
throw new TypeError(
`The valueType field of a ${item} must be either BOOLEAN, NUMERIC or TEXT. Received: ${elem.valueType}`
);
}
return elem.value;
}
export function validateFeatureType(type: string | null | undefined): FeatureType {
if (type === null || type === undefined) {
throw new Error(
`The type field of a feature must not be null or undefined. Please ensure that the type field is present and it's value correspond to either INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT`
);
}
if (typeof type !== 'string') {
throw new TypeError(
`The type field of a feature must be a string, and its value must be either INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT. Received: ${type} `
);
}
type = type.trim().toUpperCase();
if (
![
'INFORMATION',
'INTEGRATION',
'DOMAIN',
'AUTOMATION',
'MANAGEMENT',
'GUARANTEE',
'SUPPORT',
'PAYMENT',
].includes(type)
) {
throw new Error(
`The type field of a feature must be one of INFORMATION, INTEGRATION, DOMAIN, AUTOMATION, MANAGEMENT, GUARANTEE, SUPPORT or PAYMENT. Received: ${type}`
);
}
return type as FeatureType;
}
export function validateFeatureIntegrationType(
integrationType: IntegrationType | null | undefined,
featureType: FeatureType
) {
if (integrationType === null || integrationType === undefined) {
integrationType = undefined;
}
if (integrationType && typeof integrationType !== 'string') {
throw new TypeError(
`The integrationType field of a feature must be an IntegrationType ('API' | 'EXTENSION' | 'IDENTITY_PROVIDER' | 'WEB_SAAS' | 'MARKETPLACE' | 'EXTERNAL_DEVICE'). Received: ${integrationType}`
);
}else if (featureType === 'INTEGRATION' && !integrationType) {
throw new Error(
`The integrationType field of a feature of type INTEGRATION must not be null or undefined. Please ensure that the integrationType field is present and it's value correspond to either API, EXTENSION, IDENTITY_PROVIDER, WEB_SAAS, MARKETPLACE or EXTERNAL_DEVICE`
);
}else if (integrationType && typeof integrationType === "string" && !isIntegrationType(integrationType)){
throw new Error(
`The integrationType field of a feature must be one of API, EXTENSION, IDENTITY_PROVIDER, WEB_SAAS, MARKETPLACE or EXTERNAL_DEVICE. Received: ${integrationType}`
);
}
return integrationType;
}
export function validateFeatureAutomationType(automationType: AutomationType | null | undefined, featureType: FeatureType) {
if (automationType === null || automationType === undefined) {
automationType = undefined;
}
if (automationType && typeof automationType !== 'string') {
throw new TypeError(
`The automationType field of a feature must be an AutomationType ('BOT' | 'FILTERING' | 'TRACKING' | 'TASK_AUTOMATION'). Received: ${automationType}`
);
} else if (featureType === 'AUTOMATION' && !automationType) {
throw new Error(
`The automationType field of a feature of type AUTOMATION must not be null or undefined. Please ensure that the automationType field is present and its value corresponds to either BOT, FILTERING, TRACKING, or TASK_AUTOMATION`
);
} else if (automationType && typeof automationType === "string" && !isAutomationType(automationType)) {
throw new Error(
`The automationType field of a feature must be one of BOT, FILTERING, TRACKING, or TASK_AUTOMATION. Received: ${automationType}`
);
}
return automationType;
}
export function validateUnit(unit: string | null | undefined): string {
if (unit === null || unit === undefined) {
unit = '';
}
if (typeof unit !== 'string') {
throw new TypeError(`The unit field of a usage limit must be a string. Received: ${unit} `);
}
return unit;
}
export function validateUsageLimitType(type: string | null | undefined): UsageLimitType {
if (type === null || type === undefined) {
throw new Error(
`The type field of a usage limit must not be null or undefined. Please ensure that the type field is present and it's value correspond to either RENEWABLE or NON_RENEWABLE`
);
}
if (typeof type !== 'string') {
throw new TypeError(
`The type field of a usage limit must be a string, and its value must be either RENEWABLE or NON_RENEWABLE. Received: ${type} `
);
}
type = type.trim().toUpperCase();
if (!['RENEWABLE', 'NON_RENEWABLE'].includes(type)) {
throw new Error(
`The type field of a usage limit must be one of RENEWABLE or NON_RENEWABLE. Received: ${type}`
);
}
return type as UsageLimitType;
}
export function validateLinkedFeatures(
linkedFeatures: string[] | undefined | null,
pricing: Pricing
): string[] | undefined {
linkedFeatures ??= undefined;
// Check if linked features is an array
if (Array.isArray(linkedFeatures)) {
const pricingFeatures = Object.values(pricing.features).map(f => f.name);
for (const featureName of linkedFeatures) {
if (!pricingFeatures.includes(featureName)) {
throw new Error(
`The feature ${featureName}, declared as a linked feature for an usage limit, is not defined in the global features`
);
}
}
}
return linkedFeatures;
}
export function validateRenderMode(renderMode: string | undefined | null): RenderMode {
if (renderMode === null || renderMode === undefined) {
renderMode ??= 'AUTO';
}
renderMode = renderMode.toUpperCase();
if (!['AUTO', 'ENABLED', 'DISABLED'].includes(renderMode)) {
throw new Error(
`The render field of a feature or usage limit must be one of AUTO, ENABLED or DISABLED. Received: ${renderMode}`
);
}
return renderMode as RenderMode;
}
export function validatePlanFeatures(
plan: Plan,
planFeatures: Record<string, Feature>
): Record<string, Feature> {
const featuresModifiedByPlan = plan.features;
plan.features = planFeatures;
for (const planFeature of Object.values(featuresModifiedByPlan)) {
try {
if (!Object.values(planFeatures).some(f => f.name === planFeature.name)) {
throw new Error(`Feature ${planFeature.name} is not defined in the global features.`);
}
const featureWithDefaultValue = Object.values(plan.features).find(
f => f.name === planFeature.name
) as Feature;
featureWithDefaultValue.value = planFeature.value;
featureWithDefaultValue.value = validateValue(featureWithDefaultValue, 'feature');
} catch (err) {
throw new Error(
`Error while parsing the feature ${planFeature.name} of the plan ${plan.name}. Error: ${
(err as Error).message
}`
);
}
}
return plan.features;
}
export function validatePlanUsageLimits(
plan: Plan,
planUsageLimits: ContainerUsageLimits
): ContainerUsageLimits {
const usageLimitsModifiedByPlan = plan.usageLimits!;
plan.usageLimits = planUsageLimits;
for (const planUsageLimit of Object.values(usageLimitsModifiedByPlan)) {
try {
if (!Object.values(planUsageLimits).some(l => l.name === planUsageLimit.name)) {
throw new Error(
`Usage limit ${planUsageLimit.name} is not defined in the global usage limits.`
);
}
const globalUsageLimit = Object.values(planUsageLimits).find(
l => l.name === planUsageLimit.name
) as UsageLimit;
globalUsageLimit.value = planUsageLimit.value;
globalUsageLimit.value = validateValue(globalUsageLimit, 'usage limit') as string | number | boolean | undefined;
} catch (err) {
throw new Error(
`Error while parsing the usage limit ${planUsageLimit.name} of the plan ${
plan.name
}. Error: ${(err as Error).message}`
);
}
}
return plan.usageLimits;
}
export function validatePrice(
price: number | string | undefined | null,
variables: { [key: string]: any } = {}
): number | string {
if (price === null || price === undefined) {
throw new Error(
`The price field must not be null or undefined. Please ensure that the price field is present and it's a number`
);
}
if (typeof price !== 'string' && typeof price !== 'number') {
throw new TypeError(
`The price field must be a number or a string (which can contain a formula). Received: ${price}`
);
}
if (typeof price === 'number' && price < 0) {
throw new Error(`The price field must be a positive number. Received: ${price} `);
}
if (typeof price === 'string') {
if (price.includes('#')) {
for (const [variable, value] of Object.entries(variables)) {
let replacement: string;
if (typeof value === 'string') {
const escaped = (value as string).replace(/'/g, "\\'");
replacement = `'${escaped}'`;
} else if (value === null) {
replacement = 'null';
} else if (typeof value === 'object') {
// Use JSON.stringify and wrap in parentheses so object/array literals
// evaluate correctly inside eval (e.g. ({"test":"a"}).test )
replacement = `(${JSON.stringify(value)})`;
} else {
replacement = String(value);
}
price = price.replace(new RegExp(`#${variable}`, 'g'), replacement);
}
try {
// eslint-disable-next-line no-eval
const evaluatedPrice = eval(price);
if (typeof evaluatedPrice !== 'number' || isNaN(evaluatedPrice)) {
throw new Error(
`The evaluated price must result in a valid number. Current result after evaluation: ${evaluatedPrice}`
);
}
price = evaluatedPrice;
} catch (err) {
throw new Error(`Error evaluating the price formula: ${(err as Error).message}`);
}
} else if (price.match(/^[0-9]+(\.[0-9]+)?$/)) {
price = parseFloat(price);
}
}
return price;
}
export function validateAddonFeatures(
addon: AddOn,
addOnFeatures: Record<string, Feature>
): Record<string, Feature> {
for (const addOnFeature of Object.values(addon.features!)) {
try {
if (!Object.values(addOnFeatures).some(f => f.name === addOnFeature.name)) {
throw new Error(`Feature ${addOnFeature.name} is not defined in the global features.`);
}
addon.features![addOnFeature.name].value = addOnFeature.value;
addon.features![addOnFeature.name].value = validateValue(
addon.features![addOnFeature.name],
'feature',
addOnFeatures[addOnFeature.name].valueType
);
} catch (err) {
throw new Error(
`Error while parsing the feature ${addOnFeature.name} of the plan ${addon.name}. Error: ${
(err as Error).message
}`
);
}
}
return addon.features as Record<string, Feature>;
}
export function validateAddonUsageLimits(
addon: AddOn,
addonUsageLimits: ContainerUsageLimits
): ContainerUsageLimits {
for (const addonUsageLimit of Object.values(addon.usageLimits!)) {
try {
if (!Object.values(addonUsageLimits).some(l => l.name === addonUsageLimit.name)) {
throw new Error(
`Usage limit ${addonUsageLimit.name} is not defined in the global usage limits.`
);
}
if (!('value' in addonUsageLimit)) {
throw new Error(
"When declaring a new value for an usage limit or a usage limit extension within an add-on, it must be provided through the 'value' field"
);
}
addon.usageLimits![addonUsageLimit.name].value = addonUsageLimit.value;
addon.usageLimits![addonUsageLimit.name].value = validateValue(
addon.usageLimits![addonUsageLimit.name],
'usage limit',
addonUsageLimits[addonUsageLimit.name].valueType
) as string | number | boolean | undefined;
} catch (err) {
throw new Error(
`Error while parsing the usage limit ${addonUsageLimit.name} of the add-on ${
addon.name
}. Error: ${(err as Error).message}`
);
}
}
return addon.usageLimits as ContainerUsageLimits;
}
export function validateAddonUsageLimitsExtensions(
addon: AddOn,
addonUsageLimits: ContainerUsageLimits
): ContainerUsageLimits {
for (const addonUsageLimitExtension of Object.values(addon.usageLimitsExtensions!)) {
try {
if (!Object.values(addonUsageLimits).some(l => l.name === addonUsageLimitExtension.name)) {
throw new Error(
`Usage limit ${addonUsageLimitExtension.name} is not defined in the global usage limits.`
);
}
if (!('value' in addonUsageLimitExtension)) {
throw new Error(
"When declaring a new value for an usage limit or a usage limit extension within an add-on, it must be provided through the 'value' field"
);
}
addon.usageLimitsExtensions![addonUsageLimitExtension.name].value =
addonUsageLimitExtension.value;
addon.usageLimitsExtensions![addonUsageLimitExtension.name].value = validateValue(
addon.usageLimitsExtensions![addonUsageLimitExtension.name],
'usage limit',
addonUsageLimits[addonUsageLimitExtension.name].valueType
) as string | number | boolean | undefined;
} catch (err) {
throw new Error(
`Error while parsing the usage limit ${addonUsageLimitExtension.name} of the add-on ${
addon.name
}. Error: ${(err as Error).message}`
);
}
}
return addon.usageLimitsExtensions as ContainerUsageLimits;
}
export function validateAvailableFor(
availableFor: string[] | undefined | null,
pricing: Pricing
): string[] {
const planNames = pricing.plans ? Object.values(pricing.plans).map(p => p.name) : [];
availableFor ??= planNames as string[];
if (!Array.isArray(availableFor)) {
throw new TypeError(
`The availableFor field must be an array of the plan names for which the addon can be contracted. Received: ${availableFor}`
);
}
for (const planName of availableFor) {
if (!planNames.includes(planName)) {
throw new Error(`The plan ${planName} is not defined in the pricing.`);
}
}
return availableFor;
}
export function validateDependsOnOrExcludes(
fieldValue: string[] | undefined | null,
pricing: Pricing,
fieldType: 'dependsOn' | 'excludes'
): string[] {
const addonNames = pricing.addOns ? Object.values(pricing.addOns).map(a => a.name) : [];
fieldValue ??= [];
if (!Array.isArray(fieldValue)) {
throw new TypeError(
`The ${fieldType} field must be an array of the addons required to contract the addon. Received: ${fieldValue}`
);
}
return fieldValue;
}
export function postValidateDependsOnOrExclude(
fieldValue: string[] | undefined,
pricing: Pricing
): void {
const addonNames = pricing.addOns ? Object.values(pricing.addOns).map(a => a.name) : [];
if (!fieldValue) return;
for (const addonName of fieldValue) {
if (!addonNames.includes(addonName)) {
throw new Error(`The addon ${addonName} is not defined in the pricing.`);
}
}
}
export function validateTags(tags: string[] | undefined): string[] {
tags ??= [];
if (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string')) {
throw new TypeError(`The tags field must be an array of strings.`);
}
return tags;
}
export function validatePlan(plan: Plan) {
if (typeof plan === 'object' && 'features' in plan) {
return;
} else {
throw new TypeError(`The plan must be an object of type Plan`);
}
}
export function validatePlans(plans: object) {
if (!(typeof plans === 'object')) {
throw new TypeError(`The plans field must be a map of Plan objects`);
}
}
export function validateFeatures(features: object) {
if (!(typeof features === 'object') || features === null || features === undefined) {
throw new TypeError(`The features field must be a map of Feature objects`);
}
}
export function validateFeature(feature: Feature) {
if (
typeof feature === 'object' &&
'type' in feature &&
'valueType' in feature &&
'defaultValue' in feature
) {
return;
} else {
throw new TypeError(`The feature must be an object of type Feature`);
}
}
export function validateUsageLimits(usageLimits: object) {
if (!(typeof usageLimits === 'object')) {
throw new TypeError(`The usageLimits field must be a map of UsageLimit objects or undefined`);
}
}
export function validateUsageLimit(usageLimit: UsageLimit) {
if (
typeof usageLimit === 'object' &&
'type' in usageLimit &&
'valueType' in usageLimit &&
'defaultValue' in usageLimit
) {
return;
} else {
throw new TypeError(`The usage limit must be an object of type UsageLimit`);
}
}
export function validateBilling(billing: { [key: string]: number } | undefined) {
if (billing === undefined || billing === null) {
billing ??= {
monthly: 1,
};
}
if (!(typeof billing === 'object')) {
throw new TypeError(`The billing field must be an object of type {[key: string]: number}`);
}
for (const [key, value] of Object.entries(billing)) {
if (typeof value !== 'number') {
throw new TypeError(`The billing entry for ${key} must be a number. Received: ${value}`);
}
if (value <= 0 || value > 1) {
throw new Error(
`The billing entry for ${key} must be a value in the range (0,1]. Received: ${value}`
);
}
}
return billing;
}
export function validateVariables(
variables: { [key: string]: any } | undefined
): { [key: string]: any } {
variables ??= {};
if (typeof variables !== 'object') {
throw new TypeError(
`The 'variables' field must be an object of type {[key: string]: any}`
);
}
// Allow any value types for variables (object, array, string, number, boolean, etc.)
return variables;
}
export function validateUrl(url: string | undefined) {
if (url === undefined || url === null) {
url ??= undefined;
return;
}
if (typeof url !== 'string') {
throw new TypeError(`The url field must be a string. Received: ${url}`);
}
const urlPattern = /^(https?):\/\/[^\s\/$.?#].[^\s]*$/i;
if (!urlPattern.test(url)) {
throw new Error(
`The url field must be a valid URL with the http or https protocol. Received: ${url}`
);
}
return url;
}
export function validatePrivate(isPrivate: boolean | undefined) {
if (isPrivate === undefined || isPrivate === null) {
isPrivate ??= false;
}
if (typeof isPrivate !== 'boolean') {
throw new TypeError(`The private field must be a boolean. Received: ${isPrivate}`);
}
return isPrivate;
}
export function validatePricingUrls(
featureType: FeatureType,
featureIntegrationType: IntegrationType | undefined,
pricingUrls: string[] | undefined
) {
pricingUrls ??= undefined;
if (pricingUrls !== undefined) {
if (!Array.isArray(pricingUrls) || pricingUrls.some(url => typeof url !== 'string')) {
throw new TypeError(`The pricingUrls field must be an array of strings.`);
}
for (const url of pricingUrls) {
const urlPattern = /^(https?):\/\/[^^\s\/$.?#].[^\s]*$/i;
if (!urlPattern.test(url)) {
throw new Error(`The pricingUrls field must contain only valid URLs. Invalid URL: ${url}`);
}
}
if (featureType !== 'INTEGRATION' || featureIntegrationType !== 'WEB_SAAS') {
console.log(
"[WARNING] The pricingUrls field is only valid for features of 'type' INTEGRATION and 'integrationType' WEB_SAAS. The field will be ignored."
);
pricingUrls = undefined;
}
}
return pricingUrls;
}
export function validateDocUrl(featureType: FeatureType, docUrl: string | undefined) {
docUrl ??= undefined;
if (docUrl !== undefined) {
if (typeof docUrl !== 'string') {
throw new TypeError(`The docUrl field must be a string.`);
}
const urlPattern = /^(https?):\/\/[^^\s\/$.?#].[^\s]*$/i;
if (!urlPattern.test(docUrl)) {
throw new Error(`The docUrl field must be a valid URL with the http or https protocol. Received: ${docUrl}`);
}
if (featureType !== 'GUARANTEE') {
console.log(
"[WARNING] The docUrl field is only valid for features of 'type' GUARANTEE. The field will be ignored."
);
docUrl = undefined;
}
}
return docUrl;
}
export function validatePeriodUnit(periodUnit: string | undefined): "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR" {
periodUnit ??= 'MONTH';
if (typeof periodUnit !== 'string') {
throw new TypeError(`The periodUnit field must be one of the following string values: "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR". Received: ${periodUnit}`);
}
periodUnit = periodUnit.toUpperCase();
if (!["SEC", "MIN", "HOUR", "DAY", "MONTH", "YEAR"].includes(periodUnit)) {
throw new Error(
`The periodUnit field must be one of "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR". Received: ${periodUnit}`
);
}
return periodUnit as "SEC" | "MIN" | "HOUR" | "DAY" | "MONTH" | "YEAR";
}
export function validatePeriodValue(periodValue: number | undefined): number {
periodValue ??= 1;
if (typeof periodValue !== 'number') {
throw new TypeError(`The periodValue field must be a number. Received: ${periodValue}`);
}
if (periodValue <= 0) {
throw new Error(`The periodValue field must be a positive number. Received: ${periodValue}`);
}
return periodValue;
}
export function validateTrackable(trackable: boolean | undefined): boolean {
trackable ??= false;
if (typeof trackable === 'string'){
trackable = (trackable as string).toLowerCase() === 'true';
}
if (typeof trackable !== 'boolean') {
throw new TypeError(`The trackable field must be a boolean. Received: ${trackable}`);
}
return trackable;
}
export function validateSubscriptionConstraintMinQuantity(
subscriptionConstraintMinQuantity: number | undefined
): number {
subscriptionConstraintMinQuantity ??= 1;
if (typeof subscriptionConstraintMinQuantity !== 'number') {
throw new TypeError(`The subscriptionConstraintMinQuantity field must be a number. Received: ${subscriptionConstraintMinQuantity}`);
}
if (subscriptionConstraintMinQuantity < 0) {
throw new Error(`The subscriptionConstraintMinQuantity field must be a positive number. Received: ${subscriptionConstraintMinQuantity}`);
}
return subscriptionConstraintMinQuantity;
}
export function validateSubscriptionConstraintMaxQuantity(subscriptionConstraintMaxQuantity: number | undefined, minQuantity: number): number {
subscriptionConstraintMaxQuantity ??= unlimitedValue;
if (typeof subscriptionConstraintMaxQuantity !== 'number') {
throw new TypeError(`The subscriptionConstraintMaxQuantity field must be a number. Received: ${subscriptionConstraintMaxQuantity}`);
}
if (subscriptionConstraintMaxQuantity < minQuantity) {
throw new Error(`The subscriptionConstraintMaxQuantity field must be greater than or equal to the min quantity. Received: ${subscriptionConstraintMaxQuantity}`);
}
return subscriptionConstraintMaxQuantity;
}
export function validateSubscriptionConstraintQuantityStep(subscriptionConstraintQuantityStep: number | undefined, minQuantity: number): number {
subscriptionConstraintQuantityStep ??= 1;
if (typeof subscriptionConstraintQuantityStep !== 'number') {
throw new TypeError(`The subscriptionConstraintQuantityStep field must be a number. Received: ${subscriptionConstraintQuantityStep}`);
}
if (!(minQuantity % subscriptionConstraintQuantityStep === 0)) {
throw new Error(`The subscriptionConstraintQuantityStep field must be a divisor of, at least, the min quantity. Received: ${subscriptionConstraintQuantityStep}`);
}
return subscriptionConstraintQuantityStep;
}
export function validateCustom(custom: { [key: string]: any } | undefined): { [key: string]: any } {
custom ??= {};
if (typeof custom !== 'object') {
throw new TypeError(
`The custom field must be an object of type {[key: string]: any}`
);
}
return custom;
}