import _ from 'lodash'; import { Description } from 'joi'; import { ESchemaComponentType } from '../joi-types-helpers'; export interface IRule { name: string; arg?: any; args?: Record; } export interface IComponentInput { type?: ESchemaComponentType; parent?: IComponentParent; name: string; properties: Description; } export interface IValidationFlags { presence?: any; } interface IValidatorItem { validation: IValidationProperties; } interface IComponentOutputPath { outputPath?: string; } export interface IValidationProperties { type: string; } export interface ISelectOptionItem { label: string; value: string; } export interface IDisplayCondition { path: string; condition: string; value: any; } export interface IComponentSchema { key?: string; type?: ESchemaComponentType; label?: string; outputPath?: string; validators?: IValidatorItem[]; options?: ISelectOptionItem[]; displayConditions?: IDisplayCondition[]; [key: string]: any; } export interface IComponentParent { key?: string; properties: Description; } /** * This is a base component of all schema components. This class has a single public function called schema(). * This function will generate and equivalent of a Root schema component as described in the docs. * This schema() function make used of helper functions to derive component properties from the joi object. * * If you are implementing a new component that requires custom logic to derive a certain property, you can * override any of the protected functions in this class. * * Example: * export class BaseComponent { * ... * protected getLabel() : string { * ... * } * ... * } * * export class MyFancyComponent { * ... * protected override getLabel() : string { * let label = super.getLabel(); * return `fancy_${label}`; * } * ... * } * * In this example, we override the getLabel function in the base class. * We can still make use of the BaseComponent getLabel function by using super keyword. * Lastly we add our fanciness to the getLabel function. */ export class BaseComponent { protected readonly type?: ESchemaComponentType; protected readonly name: string; protected readonly parent?: IComponentParent; protected readonly properties: Description; constructor(params: IComponentInput) { this.type = params.type; this.name = params.name; this.parent = params.parent; this.properties = params.properties; this.getValidatorFromRule = this.getValidatorFromRule.bind(this); } public schema(): IComponentSchema[] { const key = this.parent?.properties?.type !== 'array' && this.parent?.key ? `${this.parent?.key}.${this.name}` : this.name; return [ { key, type: this.type, label: this.getLabel(), validators: [...this.mapJoiRulesToValidators(), ...this.mapJoiFlagsToValidators()], ...this.getOutputPath(key), ...this.getComponentCustomProperties(), ...this.getMergedMetaProperties(), }, ]; } /** * Derives the component's outputPath from meta. Otherwise use the provided key as the outputPath * Since joi meta is a list of meta objects, we always use the ouputPath found on the last meta object. * @returns The outputPath of the component */ protected getOutputPath(key: string): IComponentOutputPath { let result = key; const meta = this.properties.metas ?? this.properties.meta ?? []; if (meta.length > 0) { for (const metaItem of meta) { result = metaItem.outputPath ?? result; } } return this.parent?.properties?.type === 'array' ? {} : { outputPath: result }; } /** * Derives the component's label from its name. * @returns The label of the component */ protected getLabel(): string { return this.properties.label ?? _.capitalize(_.startCase(this.name)); } /** * Derives the component's validators from the joi given rule. * @returns The component's validator property */ protected getValidatorFromRule(rule: IRule): IValidationProperties | null { return null; } /** * Maps each rule in the joi object to a schema validator. * Each subclass of this component needs to override getValidatorFromRule * to return its supported validator based on the given rule * @returns List of validators */ private mapJoiRulesToValidators(): IValidatorItem[] { const results: IValidatorItem[] = []; for (let index = 0; index < (this.properties.rules?.length ?? 0); index += 1) { const validation = this.getValidatorFromRule(this.properties.rules[index]); if (validation) { results.push({ validation }); } } return results; } /** * Maps the flags to validators. Currently only the 'required' flag is supported by schema. * @returns List of validators */ protected mapJoiFlagsToValidators(): IValidatorItem[] { const parentProperties: Description | undefined = this.parent?.properties; const parentFlags: IValidationFlags | undefined = parentProperties?.flags as IValidationFlags; const flags: IValidationFlags | undefined = this.properties.flags as IValidationFlags; const parentIsArrayOrAlternatives = parentProperties?.type === 'array' || parentProperties?.type === 'alternatives' || !!(parentProperties as any)?.whens; const parentIsObject = parentProperties?.type === 'object'; const parentIsRequired = parentFlags?.presence === 'required'; return (!this.parent || parentIsArrayOrAlternatives || (parentIsObject && parentIsRequired)) && flags?.presence === 'required' ? [{ validation: { type: flags.presence } }] : []; } /** * Since joi meta is a list of object, this function merge all those object into a single object. * @returns The object containing all joi meta properties */ protected getMergedMetaProperties(): Record { const meta = Object.assign({}, ...(this.properties.metas ?? this.properties.meta ?? [])); delete meta.root_type; return meta; } /** * This is an abstract method that can be overriden to derive properties of a component that are custom to that component * @returns The object containing other properties of the component */ protected getComponentCustomProperties(): Record { return {}; } }