import { Description } from 'joi'; import { createSchemaComponent } from '../schema-component-factory'; import { generateSchemaFromJoi } from '../schema-generator'; import { BaseComponent, IComponentSchema, IDisplayCondition, IValidationFlags } from './base-component'; enum EAlternativeType { Then = 'then', Otherwise = 'otherwise', } export class AlternativesComponent extends BaseComponent { private get whens(): any[] { return this.properties.whens ?? this.properties.alternatives ?? []; } public override schema() { let components: IComponentSchema[] = []; if (this.whens.length === 1) { // only support length 1 alternatives const whenEntry = this.whens[0]; components = Object.keys(whenEntry).flatMap((alternative) => { const properties = whenEntry[alternative]; const alternativeType = Object.values(EAlternativeType).includes(alternative as EAlternativeType) ? (alternative as EAlternativeType) : undefined; if (!alternativeType || this.isForbiddenOrNullAllowed(properties)) { return []; } return this.generateAlternativeSchema(properties, alternativeType); }); } return components; } private getDisplayConditions(alternativeType: EAlternativeType): IDisplayCondition[] { const conditionMapping: Record = { then: '===', otherwise: '!==', }; const whenEntry = this.whens[0]; // Joi v18: ref is { path: ['field'] }, is.allow contains [{override: true}, value]. // Older/raw describes can also leave ref as a string like `ref:field`. Fall through // to an empty array when the ref has neither shape — emitting a condition with an // empty `path` would produce a broken display condition downstream, so return no // conditions instead. const refPath = typeof whenEntry.ref === 'string' ? whenEntry.ref.split(':')[1] : Array.isArray(whenEntry.ref?.path) ? whenEntry.ref.path[0] : undefined; if (!refPath) return []; const isValue = whenEntry.is?.allow ? whenEntry.is.allow.find((v: any) => v !== null && typeof v !== 'object') : whenEntry.is?.valids?.[0]; return [ { path: refPath, value: isValue, condition: conditionMapping[alternativeType], }, ]; } private isForbiddenOrNullAllowed(properties: Description) { const flags: IValidationFlags | undefined = properties?.flags as IValidationFlags; const allowList = properties?.allow ?? properties?.valids ?? []; return !properties || flags?.presence === 'forbidden' || allowList?.some(this.nullCheckCallback); } private nullCheckCallback = (item: any) => item === null; private generateAlternativeSchema(properties: Description, alternativeType: EAlternativeType): IComponentSchema[] { let components: IComponentSchema[] = []; if (properties.type === 'object') { components = generateSchemaFromJoi({ joiDescribe: properties, parent: { key: this.name, properties: this.properties }, }); } else { const component = createSchemaComponent({ name: this.name, parent: { properties: this.properties }, properties, }); components = component.schema(); } return components.map((component) => ({ ...component, ...(component.key ? { key: `${component.key}_${alternativeType}` } : {}), displayConditions: this.getDisplayConditions(alternativeType), })); } }