import { isArray, isBoolean, isEnum, isInt, isISO8601, isNegative, isNumber, isString, isUUID, length, maxLength, minLength, } from "class-validator"; import { isNull, isUndefined, keysIn } from "lodash"; import { AttachmentValue, CascadeValue, ForeignKeyValue, ForeignRowItem, GeolocationValue, LookupValue, } from "../Cell"; import { CascadeOptions, ColumnType, ColumnTypeOptions, FormulaOptions, NumberOptions, SelectOptions, TableColumn, } from "../Column"; import { CascadeFilterMode, DateFilterValue, Filter, ViewDateFilterMode, ViewFilterOperator, ViewForeignKeyFilterOperator, ViewSelectFilterOperator, ViewTextFilterOperator, } from "../Filter"; import { getColumnType, isValidId } from "."; import { getOperatorsByColumnType } from "../Filter/util"; import { IdPrefix } from "../Constant"; export function checkForeignKeyValue(cellValue: unknown): boolean { return ( isArray(cellValue) && (cellValue as ForeignKeyValue).every( (o) => isString(o.foreignRowDisplayName) && length(o.foreignRowDisplayName, 1, 1000) ) ); } export function checkCellValue( columnType: ColumnType, typeOptions: ColumnTypeOptions, cellValue: unknown ) { let testResult = true; if (columnType === ColumnType.Text) { return isString(cellValue) && maxLength(cellValue, 1000); } if (columnType === ColumnType.MultilineText) { return isString(cellValue) && maxLength(cellValue, 5000); } if (columnType === ColumnType.Checkbox) { return isNull(cellValue) || isBoolean(cellValue); } if (columnType === ColumnType.MultiCollaborator) { return ( isArray(cellValue) && (cellValue as any[]).every((o) => o === "$$CURRENT_USER$$" || isNumber(o)) ); } if (columnType === ColumnType.Collaborator) { return cellValue === "$$CURRENT_USER$$" || isNumber(cellValue); } if ( [ ColumnType.Date, ColumnType.CreatedAt, ColumnType.LastModifiedTime, ].includes(columnType) ) { return isISO8601(cellValue, { strict: true }); } if (columnType === ColumnType.Attachment) { return ( isArray(cellValue) && (cellValue as AttachmentValue).every( (o) => isUUID(o.id) && isString(o.filename) && minLength(o.filename.trim(), 1) ) ); } if (columnType === ColumnType.Number) { const numberOptions = typeOptions as NumberOptions; if (numberOptions.format === "integer") { testResult = isInt(cellValue); } else { testResult = isNumber(cellValue); } if (!numberOptions.negative) { testResult = testResult && !isNegative(cellValue); } } else if (columnType === ColumnType.ForeignKey) { return checkForeignKeyValue(cellValue); } else if ( [ ColumnType.CreatedBy, ColumnType.Collaborator, ColumnType.LastModifiedBy, ].includes(columnType) ) { return isNumber(cellValue); } else if (columnType === ColumnType.Formula) { if ((typeOptions as FormulaOptions).resultIsArray) { testResult = isArray(cellValue) && (cellValue as []).every((v) => checkCellValue( (typeOptions as FormulaOptions).resultType, typeOptions, v ) ); } else { testResult = checkCellValue( (typeOptions as FormulaOptions).resultType, typeOptions, cellValue ); } } if (columnType === ColumnType.Select) { const current = cellValue as string; const options = typeOptions as SelectOptions; if (!options.options[current]) { return false; } } if ([ColumnType.Cascade, ColumnType.MultiCascade].includes(columnType)) { const currentList = cellValue as string[]; const { options } = typeOptions as CascadeOptions; if ( !isArray(currentList) || !currentList.every((current) => options.some((v) => v.id === current)) ) { return false; } } if ([ColumnType.MultiSelect].includes(columnType)) { const currentList = cellValue as string[]; const options = typeOptions as SelectOptions; if ( !isArray(currentList) || !currentList.every((current) => options.options[current]) ) { return false; } } if (columnType === ColumnType.Formula || columnType === ColumnType.Rollup) { const { valuesByForeignRowId = {}, foreignRowIdOrder = [] } = (cellValue as LookupValue) || {}; return foreignRowIdOrder.every( (v) => ((typeOptions as FormulaOptions).resultType === "text" && isString(valuesByForeignRowId[v])) || ((typeOptions as FormulaOptions).resultType === "number" && isNumber(valuesByForeignRowId[v])) || ((typeOptions as FormulaOptions).resultType === "date" && isISO8601(valuesByForeignRowId[v])) ); } switch (columnType) { case ColumnType.GeoLocation: return ( isNumber((cellValue as GeolocationValue)?.lng) && isNumber((cellValue as GeolocationValue)?.lat) && isString((cellValue as GeolocationValue)?.city) && isString((cellValue as GeolocationValue)?.province) && isString((cellValue as GeolocationValue)?.district) && isString((cellValue as GeolocationValue)?.name) && isString((cellValue as GeolocationValue)?.address) ); default: break; } return testResult; } export const validFilter = (filter: Omit, column: TableColumn) => validFilterOperator( getColumnType(column, { lookup: true }), filter.operator ) && validFilterValue(column, filter.operator, filter.value); export const validFilterOperator = ( coltype: ColumnType, operator: ViewFilterOperator ) => { const Interface: any = getOperatorsByColumnType(coltype); const operatorList: ViewFilterOperator[] = keysIn(Interface).map( (key) => Interface[key as keyof typeof Interface] ); return operatorList.includes(operator); }; export function validFilterValue( column: TableColumn, operator: ViewFilterOperator, value: unknown ) { if ( [ ViewTextFilterOperator.IsEmpty, ViewTextFilterOperator.IsNotEmpty, ].includes(operator as ViewTextFilterOperator) ) { return true; } switch (getColumnType(column, { lookup: true })) { case ColumnType.Text: case ColumnType.Url: case ColumnType.MultilineText: case ColumnType.Button: case ColumnType.GeoLocation: return isString(value); case ColumnType.ForeignKey: { switch (operator) { case ViewForeignKeyFilterOperator.Like: case ViewForeignKeyFilterOperator.NotLike: return isString(value); case ViewForeignKeyFilterOperator.Is: case ViewForeignKeyFilterOperator.IncludeAll: case ViewForeignKeyFilterOperator.In: case ViewForeignKeyFilterOperator.NotIn: return ( isArray(value) && (value).every((o) => isValidId(IdPrefix.Row, o.foreignRowId) ) ); default: return false; } } case ColumnType.Formula: { if ( ["number"].includes((column.typeOptions as FormulaOptions).resultType) ) { return isNumber(value); } if ( ["date", "text"].includes( (column.typeOptions as FormulaOptions).resultType ) ) { return isString(value); } return false; } case ColumnType.Checkbox: return isBoolean(value); case ColumnType.Number: case ColumnType.Percent: case ColumnType.Currency: case ColumnType.AutoNumber: { return isNumber(value); } case ColumnType.CreatedBy: case ColumnType.LastModifiedBy: { if ( [ViewSelectFilterOperator.In, ViewSelectFilterOperator.NotIn].includes( operator as ViewSelectFilterOperator ) ) { return ( isArray(value) && (value).every( (o) => o === ("$$CURRENT_USER$$" as any) || isNumber(o) ) ); } return ( value === ("$$CURRENT_USER$$" as any) || isNumber(value) || isNull(value) ); } case ColumnType.DataStatus: case ColumnType.Select: { if ( [ViewSelectFilterOperator.Is, ViewSelectFilterOperator.IsNot].includes( operator as any ) ) { return isString(value) || isNull(value); } return ( isNull(value) || (isArray(value) && (value).every((o) => isString(o))) ); } case ColumnType.MultiSelect: { return ( isNull(value) || (isArray(value) && (value).every((o) => isString(o))) ); } case ColumnType.Collaborator: { if ( [ViewSelectFilterOperator.In, ViewSelectFilterOperator.NotIn].includes( operator as any ) ) { return ( isArray(value) && (value).every( (o) => o === ("$$CURRENT_USER$$" as any) || isNumber(o) ) ); } return ( isNull(value) || isNumber(value) || value === ("$$CURRENT_USER$$" as any) ); } case ColumnType.MultiCollaborator: { return ( isArray(value) && (value).every( (o) => isNumber(o) || o === ("$$CURRENT_USER$$" as any) ) ); } case ColumnType.Attachment: { return isString(value); } case ColumnType.Date: case ColumnType.LastModifiedTime: case ColumnType.CreatedAt: { switch ((value as DateFilterValue)?.mode) { case ViewDateFilterMode.exactDate: { return isString((value as DateFilterValue).data); } case ViewDateFilterMode.yesterday: case ViewDateFilterMode.today: case ViewDateFilterMode.tomorrow: case ViewDateFilterMode.oneWeekAgo: case ViewDateFilterMode.PastWeek: case ViewDateFilterMode.oneWeekFromNow: case ViewDateFilterMode.NextWeek: case ViewDateFilterMode.oneMonthAgo: case ViewDateFilterMode.PastMonth: case ViewDateFilterMode.NextMonth: { return true; } case ViewDateFilterMode.numberOfDaysAgo: case ViewDateFilterMode.LastNumberOfDays: case ViewDateFilterMode.NextNumberOfDays: case ViewDateFilterMode.numberOfDaysFromNow: case ViewDateFilterMode.PastNumberOfDays: { return isNumber((value as DateFilterValue).numberOfDays); } default: return false; } } case ColumnType.MultiCascade: case ColumnType.Cascade: { return ( (isEnum((value as any)?.mode, CascadeFilterMode) && (value as any)?.data?.every((o: any) => isString(o))) || (value as CascadeValue)?.every((o) => isString(o)) ); } default: return false; } }