/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { filterMap, SqlExpression } from 'druid-query-toolkit'; import type { TransferValue } from './host-store'; import type { ExpressionMeta } from './models'; import type { OptionValue, Parameter, ParameterDefinitions, ParameterTypes } from './parameter'; type ParameterValue = ParameterTypes[keyof ParameterTypes]; /** * Computes a record of parameters values out of values provided by controls, transferrable values, * parameters definitions and possible columns. This function is the layer between the UI and the visual module, * it makes sure the visual module receives sanitized parameters values (and maybe values from the previous module through * transfer state). * * @param parametersValues the values * @param parameters the parameters definitions * @param transferState a record taht might contain transferable values * @param columns the available columns (used to validate certain values) * @returns a record of the paremeters values, inflated, transferred and validated */ export function inflateValidateAndFillDefaults( parametersValues: Record, parameters: ParameterDefinitions | undefined, transferState: Record, columns: ExpressionMeta[], ): Record { const inflatedParametersValues: Record = {}; if (!parameters) return inflatedParametersValues; for (const [paramName, parameter] of Object.entries(parameters)) { const transferGroup = parameter.control?.transferGroup; const transferValue = transferGroup ? transferState[transferGroup] : undefined; let paramValue = applyTransferValue(transferValue, parameter) ?? parametersValues[paramName]; if (typeof paramValue !== 'undefined') { paramValue = inflateAndValidateParamValue(paramValue, parameter, columns); } if (typeof paramValue === 'undefined') { paramValue = getEffectiveDefault(parameter, columns); } if (typeof paramValue !== 'undefined') { inflatedParametersValues[paramName] = paramValue; } } const cleanedUpParametersValues: Record = {}; // Looping over parameters a second time because we need the up-to-date inflatedParams // for the potential `defined` functions on each of them for (const [paramName, parameter] of Object.entries(parameters)) { if ( parameter.defined !== undefined && !parameter.defined({ parametersValues: inflatedParametersValues }) ) continue; cleanedUpParametersValues[paramName] = inflatedParametersValues[paramName]; } return cleanedUpParametersValues; } function applyTransferValue( transferValue: TransferValue | undefined, parameter: Parameter, ): ParameterValue | undefined { if (!transferValue) return; const [type, value] = transferValue; switch (type) { case 'aggregate': { const ed = inflateExpressionMeta(value); if (!ed || (parameter.type !== 'aggregate' && parameter.type !== 'aggregates')) { return; } return parameter.type === 'aggregate' ? ed : [ed]; } case 'aggregates': { const eds = inflateExpressionMetas(value); if (eds.length === 0 || (parameter.type !== 'aggregate' && parameter.type !== 'aggregates')) { return; } return parameter.type === 'aggregate' ? eds[0] : eds; } case 'column': { const ed = inflateExpressionMeta(value); if (!ed || (parameter.type !== 'column' && parameter.type !== 'columns')) { return; } return parameter.type === 'column' ? ed : [ed]; } case 'columns': { const eds = inflateExpressionMetas(value); if (eds.length === 0 || (parameter.type !== 'column' && parameter.type !== 'columns')) { return; } return parameter.type === 'column' ? eds[0] : eds; } case 'option': { if (parameter.type === 'option') { return value as OptionValue; } if (parameter.type === 'options') { return [value as OptionValue]; } return; } case 'options': { if (!Array.isArray(value) || value.length === 0) return; if (parameter.type === 'option') { return value[0]; } if (parameter.type === 'options') { return value; } return; } default: return value as any; } } function inflateAndValidateParamValue( value: unknown, parameter: Parameter, columns: ExpressionMeta[], ): ParameterValue | undefined { switch (parameter.type) { case 'boolean': return Boolean(value); case 'number': { let v = Number(value); if (isNaN(v)) v = 0; if (typeof parameter.min === 'number') { v = Math.max(v, parameter.min); } if (typeof parameter.max === 'number') { v = Math.min(v, parameter.max); } return v; } case 'option': if (!parameter.options || !parameter.options.includes(value as OptionValue)) return; return value as OptionValue; case 'options': { if (!Array.isArray(value)) return []; const options = parameter.options || []; return value.filter(v => options.includes(v)); } case 'column': case 'aggregate': { const ed = inflateExpressionMeta(value); if (!validateExpressionMeta(ed, columns)) return; return ed; } case 'columns': case 'aggregates': return inflateExpressionMetas(value).filter(ed => validateExpressionMeta(ed, columns)); default: return value as any; } } function getEffectiveDefault(parameter: Parameter, columns: ExpressionMeta[]): any { const effectiveDefault = parameter.default; if (typeof effectiveDefault !== 'undefined') { return inflateAndValidateParamValue(effectiveDefault, parameter, columns); } switch (parameter.type) { case 'boolean': return false; case 'option': { const controlOptions = parameter.options || []; if (!controlOptions.length) return null; return controlOptions[0]; } case 'string': return ''; case 'options': case 'columns': case 'aggregates': return []; default: return; } } function inflateExpressionMeta(value: any): ExpressionMeta | undefined { if (!value) return; const expression = SqlExpression.maybeParse(value.expression); if (!expression) return; return { expression, name: String(value.name), sqlType: value.sqlType, multiValue: Boolean(value.multiValue), }; } function inflateExpressionMetas(value: any): ExpressionMeta[] { if (!Array.isArray(value)) return []; return filterMap(value, inflateExpressionMeta); } function validateExpressionMeta( e: ExpressionMeta | undefined, columns: ExpressionMeta[], ): e is ExpressionMeta { if (!e) return false; return expressionWithinColumns(e.expression, columns); } function expressionWithinColumns(ex: SqlExpression, columns: ExpressionMeta[]): boolean { const usedColumns = ex.getUsedColumnNames(); return usedColumns.every(columnName => columns.some(c => c.name === columnName)); }