src/app/shared/dc-validator/dc-validator.ts

Index

Properties
Methods

Constructor

constructor(sasparams: SASParam, $dataFormats: $DataFormats, cols: Col[], dqRules: DQRule[], dqData: DQData[], hotInstance?: Handsontable)
Parameters :
Name Type Optional
sasparams SASParam No
$dataFormats $DataFormats No
cols Col[] No
dqRules DQRule[] No
dqData DQData[] No
hotInstance Handsontable Yes

Properties

Private dqdata
Type : DQData[]
Default value : []
Private dqrules
Type : DQRule[]
Default value : []
executeHotValidator
Type : unknown
Default value : () => {...}

It will execute validator, by creating the dummy instance of HOT because validator function requires it in it's context We need this function to be able to REUSE the validation that is done in HOT For exmaple in EDIT RECROD MODAL

Parameters :
Name Description
columnRules

column rule for which to run validation

value

value against which validation is done

callback

true if valid - false if invalid

Private hiddenColumns
Type : number[]
Default value : []
Private hotInstance
Type : Handsontable | undefined
Private primaryKeys
Type : string[]
Default value : []
Private rules
Type : DcValidation[]
Default value : []
Private sasparams
Type : SASParam | undefined

Methods

Private addPrimaryKeyNotNullRules
addPrimaryKeyNotNullRules()

Adds a NOTNULL rule for each primary key column that does not already have one. A primary key identifies the row, so a blank or a special missing there is never valid.

Returns : void
failsSoftRegex
failsSoftRegex(col: string, value: any)

Whether a value fails a SOFTREGEX rule on the given column, if one exists. SOFTREGEX never goes through dqValidate/the cell validator (see setupValidations — it's a display-only grid renderer instead), so the edit-record modal, which has no grid renderer to hook into, uses this directly to show the same warning outside the grid.

A column can carry both HARDREGEX and SOFTREGEX at once, but only one regex ever runs per column: if a HARDREGEX rule exists, SOFTREGEX is ignored entirely - the cell is already governed by the blocking rule, so a yellow warning on top would be redundant, even for values that pass the hard rule. Same precedence as makeRegexWarningRenderer.

Parameters :
Name Type Optional
col string No
value any No
Returns : boolean
getDqDetails
getDqDetails(col?: string)

DQ Rules values comes from MPE_VALIDATIONS table

Parameters :
Name Type Optional Description
col string Yes

column name (optional) - if not provided all DQ Rules will be returned

Returns : DQRule[]

array of DQRUles for given col name

getDqDropdownSource
getDqDropdownSource(rule: DcValidation)

Retrieves dropdown source for given dc validation rule The values comes from MPE_SELECTBOX table

Parameters :
Name Type Optional
rule DcValidation No
Returns : [] | []
getFormulaRuleValue
getFormulaRuleValue(col: string)

Returns the RULE_VALUE of a HARDFORMULA/SOFTFORMULA rule on the given column, for display in the column-header info dropdown. A column only ever carries one of the two in practice (readonly-computed vs. overridable-default aren't a meaningful combination), so unlike getRegexRuleValues there's no need to distinguish which one this is.

Parameters :
Name Type Optional Description
col string No

column name

Returns : string | undefined
Private getHandsontableValidator
getHandsontableValidator(type: string)
Parameters :
Name Type Optional
type string No
Returns : any
getHiddenColumns
getHiddenColumns()
Returns : number[]
getNotNullDefaultValue
getNotNullDefaultValue(col: string)

Returns the RULE_VALUE for a NOTNULL rule on the given column. Used for auto-populating default values when cells are empty. Converts to number for numeric columns.

Parameters :
Name Type Optional Description
col string No

column name

Returns : string | number | undefined

RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined

getRegexRuleValues
getRegexRuleValues(col: string)

Returns the RULE_VALUEs of a HARDREGEX/SOFTREGEX rule on the given column, for display in the column-header info dropdown. Both are returned raw - the caller (buildColInfoHtml) decides which one is actually applied (HARDREGEX wins when both exist).

Parameters :
Name Type Optional Description
col string No

column name

getRoundDigits
getRoundDigits(col: string)

Returns the num_digits for a ROUND rule on the given column, used to round edited/pasted values Excel-style. Returns undefined if no ROUND rule exists or its RULE_VALUE is not an integer.

Parameters :
Name Type Optional Description
col string No

column name

Returns : number | undefined
getRule
getRule(selector: string | number)
Parameters :
Name Type Optional
selector string | number No
Returns : DcValidation | undefined
Private getRuleIndex
getRuleIndex(col: string | number)
Parameters :
Name Type Optional
col string | number No
Returns : number
getRules
getRules()
Returns : DcValidation[]
hasDqRules
hasDqRules(col: string, dqrules: DQRuleTypes[], matchAll: boolean)

Checks if the given column has the provided dqrules

Parameters :
Name Type Optional Default value Description
col string No
dqrules DQRuleTypes[] No

returns true if any of provided rules match

matchAll boolean No false
Returns : boolean
isDqCol
isDqCol(col: string)

Checks if given column is or is not included in the current DATA QUALITY rules

Parameters :
Name Type Optional Description
col string No

column name

Returns : boolean
Private lengthCheck
lengthCheck(value: any, ruleLength: number, colType: string)
Parameters :
Name Type Optional
value any No
ruleLength number No
colType string No
Returns : boolean
registerCustomEditors
registerCustomEditors()
Returns : void
removeRule
removeRule(selector: string | number)
Parameters :
Name Type Optional
selector string | number No
Returns : void
Private setColumnLevelSecurity
setColumnLevelSecurity(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
Private setDefaultValidator
setDefaultValidator(self: any, i: number)
Parameters :
Name Type Optional
self any No
i number No
Returns : void
Private setupValidations
setupValidations()

It will populate columns dropdowns for which data is found Also validator for every column is assigned

Returns : void
updateDqData
updateDqData()

SOFTSELECT is not defined in DQ RULES This function fetches it's values and pushes in the DQ RULES array with SOFTSELECT type Those values are used also for HARDSELECT, only difference is HARDSELECT has strict=true

Returns : void
updateRule
updateRule(selector: string | number, value: DcValidationRuleUpdate)
Parameters :
Name Type Optional
selector string | number No
value DcValidationRuleUpdate No
Returns : void
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import Handsontable from 'handsontable'
import { CellProperties } from 'handsontable/settings'
import {
  autocompleteValidator,
  intlDateValidator,
  intlTimeValidator
} from 'handsontable/validators'
import { $DataFormats } from 'src/app/models/sas/editors-getdata.model'
import { DQData, SASParam } from '../../models/TableData'
import { Col } from './models/col.model'
import {
  DcValidation,
  DcValidationRuleUpdate
} from './models/dc-validation.model'
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
import { getDqDataCols } from './utils/getDqDataCols'
import { getNotNullDefault } from './utils/getNotNullDefault'
import { mergeColsRules } from './utils/mergeColsRules'
import { parseColTypeRow } from './utils/parseColTypeRow'
import { DELETE_RECORD_COLUMN_RULE } from './utils/deleteRecordColumnRule'
import { EDIT_STATUS_COLUMN_RULE } from './utils/editStatusColumnRule'
import { dqValidate } from './validations/dq-validation'
import {
  datetimeValidator,
  specialMissingNumericValidator
} from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats'
import { mapIntlCellTypes } from './utils/mapIntlCellTypes'
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
import { registerIntlCellTypes } from './cellTypes/intlCellTypes'
import { makeNumberFormatRenderer } from '../../editor/utils/renderers.utils'
import { makeRegexWarningRenderer } from '../../editor/utils/regex-warning-renderer'
import { isRegexRuleExempt } from './utils/isRegexRuleExempt'
import { parseRegexRule } from './utils/parseRegexRule'

export class DcValidator {
  private rules: DcValidation[] = []
  private dqrules: DQRule[] = []
  private dqdata: DQData[] = []
  private sasparams: SASParam | undefined
  private hiddenColumns: number[] = []
  private primaryKeys: string[] = []
  private hotInstance: Handsontable | undefined

  constructor(
    sasparams: SASParam,
    $dataFormats: $DataFormats,
    cols: Col[],
    dqRules: DQRule[],
    dqData: DQData[],
    hotInstance?: Handsontable
  ) {
    this.registerCustomEditors()

    this.sasparams = sasparams
    this.hotInstance = hotInstance
    // Each cols[i].COLTYPE is a single JSON-object string (one per column, as
    // of the vertical-array getdata.sas fix — issue #253), not the old
    // comma-joined sasparams.COLTYPE list. DELETE_RECORD_COLUMN_RULE is
    // injected separately — see its own doc comment for why.
    //
    // COLHEADERS is the authoritative grid order and the only safe key to
    // build against: it is the variable list of the *output* dataset, and the
    // editor prepends the client-owned delete column to it (see
    // editor.component.ts's initSetup) before this constructor runs, whereas
    // cols[].VARNUM describes the *source* table and cols[] itself can arrive
    // sorted by NAME. Those orders diverge whenever the PK isn't already the
    // source's first column, so rules are keyed by name and emitted in
    // COLHEADERS order — never paired positionally against cols[] or VARNUM.
    //
    // A column with no parseable COLTYPE falls back to a plain text rule
    // rather than being dropped: getRules() is consumed positionally against
    // COLHEADERS, so a missing entry would shift every later column.
    const ruleByName = new Map<string, DcValidation>([
      [DELETE_RECORD_COLUMN_RULE.data, { ...DELETE_RECORD_COLUMN_RULE }]
    ])
    for (const col of cols) {
      const rule = parseColTypeRow(col.COLTYPE)
      if (rule) ruleByName.set(col.NAME, rule)
    }
    this.rules = sasparams.COLHEADERS.split(',').map(
      (name) => ruleByName.get(name) ?? { data: name }
    )
    this.rules = mergeColsRules(cols, this.rules, $dataFormats)
    this.rules = applyNumericFormats(this.rules)
    this.rules = mapIntlCellTypes(this.rules)

    // EDIT_STATUS is appended last, always hidden - it's a purely
    // client-synthesized column (never in COLHEADERS) that gives
    // DC.ROW_STATUS a real cell to reference. See its own doc comment.
    this.rules.push({ ...EDIT_STATUS_COLUMN_RULE })
    this.hiddenColumns.push(this.rules.length - 1)

    this.dqrules = [...dqRules]
    this.dqdata = dqData
    this.primaryKeys = sasparams.PK.split(' ')

    // A primary key is NOT NULL by definition, so it must reject a blank and
    // a special missing (".A"-".Z", "._") even when the target table carries
    // no physical NOT NULL constraint and MPE_VALIDATIONS has no NOTNULL rule
    // for it.  Synthesised here so every keyed table gets it, and so the grid,
    // the edit-record modal and Excel upload validation all see the same rule.
    this.addPrimaryKeyNotNullRules()

    this.updateDqData()
    this.setupValidations()
  }

  /**
   * Adds a NOTNULL rule for each primary key column that does not already
   * have one.  A primary key identifies the row, so a blank or a special
   * missing there is never valid.
   */
  private addPrimaryKeyNotNullRules(): void {
    for (const pk of this.primaryKeys) {
      if (!pk) continue

      // A buskey can name a column the table no longer has - do not
      // synthesise a rule for a column that is not in the grid.
      if (!this.rules.some((rule) => rule.data === pk)) continue

      const hasNotNull = this.dqrules.some(
        (rule) => rule.BASE_COL === pk && rule.RULE_TYPE === 'NOTNULL'
      )

      if (!hasNotNull) {
        this.dqrules.push({
          BASE_COL: pk,
          RULE_TYPE: 'NOTNULL',
          RULE_VALUE: '',
          X: 1
        })
      }
    }
  }

  registerCustomEditors() {
    Handsontable.editors.registerEditor(
      'autocomplete.custom',
      CustomAutocompleteEditor
    )
    registerIntlCellTypes()
  }

  getRules(): DcValidation[] {
    return this.rules
  }

  getRule(selector: string | number): DcValidation | undefined {
    const index = this.getRuleIndex(selector)

    return this.rules[index]
  }

  getHiddenColumns(): number[] {
    return this.hiddenColumns
  }

  updateRule(selector: string | number, value: DcValidationRuleUpdate) {
    let index = this.getRuleIndex(selector)

    this.rules[index] = {
      ...this.rules[index],
      ...value
    }
  }

  removeRule(selector: string | number) {
    const index = this.getRuleIndex(selector)

    this.rules.splice(index, 1)
  }

  /**
   * DQ Rules values comes from MPE_VALIDATIONS table
   * @param col column name (optional) - if not provided all DQ Rules will be returned
   * @returns array of DQRUles for given col name
   */
  getDqDetails(col?: string): DQRule[] {
    if (!col) return this.dqrules

    return this.dqrules.filter((dqrule: DQRule) => dqrule.BASE_COL === col)
  }

  /**
   * Checks if given column is or is not included in the current DATA QUALITY rules
   *
   * @param col column name
   */
  isDqCol(col: string): boolean {
    // My refactored solution
    return !!this.dqrules.find((dqrule: DQRule) => dqrule.BASE_COL === col)
  }

  /**
   * Checks if the given column has the provided dqrules
   * @param dqrules returns true if any of provided rules match
   */
  hasDqRules(
    col: string,
    dqrules: DQRuleTypes[],
    matchAll: boolean = false
  ): boolean {
    if (matchAll) {
      return (
        this.dqrules.filter(
          (x) => x.BASE_COL === col && dqrules.includes(x.RULE_TYPE)
        ).length === dqrules.length
      )
    } else {
      return !!this.dqrules.find(
        (x) => x.BASE_COL === col && dqrules.includes(x.RULE_TYPE)
      )
    }
  }

  /**
   * Returns the RULE_VALUE for a NOTNULL rule on the given column.
   * Used for auto-populating default values when cells are empty.
   * Converts to number for numeric columns.
   *
   * @param col column name
   * @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
   */
  getNotNullDefaultValue(col: string): string | number | undefined {
    const colRule = this.getRule(col)
    return getNotNullDefault(col, this.dqrules, colRule?.type)
  }

  /**
   * Returns the num_digits for a ROUND rule on the given column, used to
   * round edited/pasted values Excel-style. Returns undefined if no ROUND
   * rule exists or its RULE_VALUE is not an integer.
   *
   * @param col column name
   */
  getRoundDigits(col: string): number | undefined {
    const roundRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'ROUND'
    )

    if (!roundRule) return undefined

    const digits = parseInt(roundRule.RULE_VALUE, 10)

    return isNaN(digits) ? undefined : digits
  }

  /**
   * Returns the RULE_VALUEs of a HARDREGEX/SOFTREGEX rule on the given
   * column, for display in the column-header info dropdown. Both are
   * returned raw - the caller (buildColInfoHtml) decides which one is
   * actually applied (HARDREGEX wins when both exist).
   *
   * @param col column name
   */
  getRegexRuleValues(col: string): {
    hardRegexValue: string | undefined
    softRegexValue: string | undefined
  } {
    const hardRegexRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'HARDREGEX'
    )
    const softRegexRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
    )

    return {
      hardRegexValue: hardRegexRule?.RULE_VALUE,
      softRegexValue: softRegexRule?.RULE_VALUE
    }
  }

  /**
   * Returns the RULE_VALUE of a HARDFORMULA/SOFTFORMULA rule on the given
   * column, for display in the column-header info dropdown. A column only
   * ever carries one of the two in practice (readonly-computed vs.
   * overridable-default aren't a meaningful combination), so unlike
   * getRegexRuleValues there's no need to distinguish which one this is.
   *
   * @param col column name
   */
  getFormulaRuleValue(col: string): string | undefined {
    const formulaRule = this.dqrules.find(
      (rule: DQRule) =>
        rule.BASE_COL === col &&
        (rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA')
    )

    return formulaRule?.RULE_VALUE
  }

  /**
   * Retrieves dropdown source for given dc validation rule
   * The values comes from MPE_SELECTBOX table
   * @param rule
   */
  getDqDropdownSource(rule: DcValidation): string[] | number[] {
    let details: any[] = []

    this.dqrules.forEach((element: DQRule) => {
      if (
        element.BASE_COL.toString() === rule.data &&
        rule.data.toString() &&
        [
          'HARDSELECT',
          'SOFTSELECT',
          'HARDSELECT_HOOK',
          'SOFTSELECT_HOOK'
        ].includes(element.RULE_TYPE.toString())
      ) {
        this.dqdata
          .filter((dqdata: DQData) => dqdata.BASE_COL === element.BASE_COL)
          .forEach((data: any) => {
            /**
             * If column type is numeric, we want to populate dropdown with numeric array
             * So we will convert it before pushing to array.
             */
            if (rule.type && rule.type === 'numeric') {
              // A special missing reaches us as a bare letter ("A", "_"), and
              // the regular missing as "." - Number() would turn either into
              // NaN, so a strict (HARDSELECT) dropdown could never accept a
              // value the column actually holds.  Keep them as they are.
              const rawValue = data['RULE_DATA']
              details.push(
                isSpecialMissing(rawValue) ? rawValue : Number(rawValue)
              )
            } else {
              details.push(data['RULE_DATA'])
            }
          })
      }
    })

    return details
  }

  /**
   * Whether a value fails a SOFTREGEX rule on the given column, if one
   * exists. SOFTREGEX never goes through dqValidate/the cell validator (see
   * setupValidations — it's a display-only grid renderer instead), so the
   * edit-record modal, which has no grid renderer to hook into, uses this
   * directly to show the same warning outside the grid.
   *
   * A column can carry both HARDREGEX and SOFTREGEX at once, but only one
   * regex ever runs per column: if a HARDREGEX rule exists, SOFTREGEX is
   * ignored entirely - the cell is already governed by the blocking rule,
   * so a yellow warning on top would be redundant, even for values that
   * pass the hard rule.  Same precedence as makeRegexWarningRenderer.
   */
  failsSoftRegex(col: string, value: any): boolean {
    const isNumeric =
      this.rules.find((rule) => rule.data === col)?.type === 'numeric'
    if (isRegexRuleExempt(value, isNumeric)) return false

    const hardRegexRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'HARDREGEX'
    )
    if (hardRegexRule) return false

    const softRegexRule = this.dqrules.find(
      (rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
    )
    if (!softRegexRule) return false

    try {
      return !parseRegexRule(softRegexRule.RULE_VALUE).test(value.toString())
    } catch (e) {
      return false
    }
  }

  /**
   * SOFTSELECT is not defined in DQ RULES
   * This function fetches it's values and pushes in the DQ RULES array with SOFTSELECT type
   * Those values are used also for HARDSELECT, only difference is HARDSELECT has strict=true
   */
  updateDqData() {
    if (this.dqdata.length > 0) {
      let dqDataCols: string[] = getDqDataCols(this.dqdata)

      dqDataCols.forEach((dqCol: string) => {
        let rulePresent = false

        if (this.dqrules.length > 0) {
          this.dqrules.forEach((dqRule: DQRule) => {
            if (
              dqRule.BASE_COL === dqCol &&
              (dqRule.RULE_TYPE === 'SOFTSELECT' ||
                dqRule.RULE_TYPE === 'HARDSELECT')
            ) {
              rulePresent = true
            }
          })
        }

        if (!rulePresent) {
          this.dqrules.push({
            BASE_COL: dqCol,
            RULE_TYPE: 'SOFTSELECT',
            RULE_VALUE: dqCol,
            X: 1
          })
        }
      })
    }
  }

  /**
   * It will execute validator, by creating the dummy instance of HOT
   * because validator function requires it in it's context
   * We need this function to be able to REUSE the validation that is done in HOT
   * For exmaple in EDIT RECROD MODAL
   * @param columnRules column rule for which to run validation
   * @param value value against which validation is done
   * @param callback true if valid - false if invalid
   */
  executeHotValidator = (
    columnRules: DcValidation,
    value: any,
    callback?: (valid: boolean) => void
  ) => {
    const cellProperties: CellProperties = {
      ...columnRules,
      validator: undefined,
      row: 0,
      col: 0,
      instance: new Handsontable(document.createElement('div'), {}),
      visualRow: 0,
      visualCol: 0,
      prop: 0
    }

    if (value === undefined || value === null) value = ''

    if (
      !columnRules ||
      !columnRules.validator ||
      typeof columnRules.validator !== 'function'
    ) {
      if (callback) callback(false)
      return
    }

    columnRules.validator.call(cellProperties, value, (valid: boolean) => {
      if (callback) callback(valid)
    })
  }

  /**
   * It will populate columns dropdowns for which data is found
   * Also validator for every column is assigned
   */
  private setupValidations() {
    for (let i = 0; i < this.rules.length; i++) {
      const ruleColName = this.rules[i].data || ''

      if (ruleColName === '_____DELETE__THIS__RECORD_____') {
        continue
      }

      /**
       * Populate dropdowns SOFTSELECT, HARDSELECT
       */
      if (this.isDqCol(ruleColName)) {
        let source: string[] | number[] = this.getDqDropdownSource(
          this.rules[i]
        )

        if (source.length > 0) {
          // For SAS num cols keep type='numeric' so HOT's numericEditor /
          // numericRenderer + numbro coercion stay alive — same per-column
          // model as the per-cell pattern in
          // editor.component.ts:reSetCellValidationValues().
          this.rules[i].source = source
          this.rules[i].editor = 'autocomplete.custom'
          this.rules[i].renderer = 'autocomplete'
          this.rules[i].filter = false

          if (this.rules[i].sasType !== 'num') {
            this.rules[i].type = 'autocomplete'
          }
        }

        if (this.hasDqRules(ruleColName, ['SOFTSELECT'])) {
          this.rules[i].strict = false
        }

        if (this.hasDqRules(ruleColName, ['HARDSELECT'])) {
          this.rules[i].strict = true
        }

        if (this.hasDqRules(ruleColName, ['SOFTSELECT_HOOK'])) {
          this.rules[i].strict = false
        }

        if (this.hasDqRules(ruleColName, ['HARDSELECT_HOOK'])) {
          this.rules[i].strict = true
        }

        if (this.hasDqRules(ruleColName, ['NOTNULL'])) {
          this.rules[i].allowEmpty = false
        }

        // READONLY: render column read-only (default value handled via getColumnDefault on add row)
        if (this.hasDqRules(ruleColName, ['READONLY'])) {
          this.rules[i].readOnly = true
        }

        // HARDFORMULA: same read-only treatment as an explicit READONLY rule
        // - its value is always computed (see applyFormulaRules), never
        // user-editable. SOFTFORMULA deliberately does not set this: its
        // computed value is only a default, the user can overwrite it.
        if (this.hasDqRules(ruleColName, ['HARDFORMULA'])) {
          this.rules[i].readOnly = true
        }

        // HIDDEN: hide column in HOT but keep its data (still submitted via hot.getData())
        if (this.hasDqRules(ruleColName, ['HIDDEN'])) {
          this.hiddenColumns.push(i)
        }

        // NUMBER_FORMAT: display-only Intl.NumberFormat renderer.
        // Set last so it overrides a dropdown's 'autocomplete' renderer (last-wins).
        if (this.hasDqRules(ruleColName, ['NUMBER_FORMAT'])) {
          const fmtRule = this.getDqDetails(ruleColName).find(
            (rule: DQRule) => rule.RULE_TYPE === 'NUMBER_FORMAT'
          )
          this.rules[i].renderer = makeNumberFormatRenderer(fmtRule?.RULE_VALUE)
          // Clear numericFormat (set by applyNumericFormats on every numeric
          // column). On a numeric cell HOT's built-in numeric renderer would
          // re-format via numericFormat, overriding our custom NUMBER_FORMAT
          // Intl renderer and dropping the currency symbol. The numeric
          // editor/validator stay intact via `type`.
          this.rules[i].numericFormat = undefined
        }

        // HARDREGEX/SOFTREGEX: submission-blocking for HARDREGEX still goes
        // through the normal validator/dqValidate path (see dq-validation.ts)
        // and is unaffected by this renderer. This only wires the display
        // layer - a 'REGEX: <pattern>' title, plus a yellow dc-warning-cell
        // when only SOFTREGEX fails (never for HARDREGEX, which relies on
        // HOT's own red htInvalid instead). Last-wins against NUMBER_FORMAT
        // if a column somehow carried both (not expected in practice — one
        // formats numbers, the other pattern-matches text).
        if (this.hasDqRules(ruleColName, ['HARDREGEX', 'SOFTREGEX'])) {
          const { hardRegexValue, softRegexValue } =
            this.getRegexRuleValues(ruleColName)
          this.rules[i].renderer = makeRegexWarningRenderer(
            softRegexValue,
            hardRegexValue,
            this.rules[i].type === 'numeric'
          )
        }
      }

      const self = this

      this.setDefaultValidator(self, i)
      this.setColumnLevelSecurity(i)
    }

    // If CLS is true, we hide `delete column`
    if (this.sasparams?.CLS_FLAG) this.hiddenColumns.push(0)
  }

  private setDefaultValidator(self: any, i: number) {
    this.rules[i].validator = function (
      value: any,
      callback: (valid: boolean) => void
    ) {
      const col = self.rules[i].data?.toString()
      const colType = self.rules[i].type || ''
      let handsontableValid = null

      // A row marked for delete is exempt from cell-level validation — its
      // values are about to be removed. Two deliberate exceptions:
      // - PK columns still validate: the key identifies WHICH record to
      //   delete, and blank/invalid PKs are also skipped by the separate
      //   duplicate-key check (validatePrimaryKeys), so this is the only
      //   net that would otherwise catch them.
      // - Uniqueness checks are unaffected by construction: they run in a
      //   separate pass (editor.component.ts validatePrimaryKeys) that
      //   never goes through this validator.
      const markedForDelete =
        this.instance?.getDataAtRowProp?.(
          this.row,
          '_____DELETE__THIS__RECORD_____'
        ) === 'Yes'

      if (markedForDelete && !self.primaryKeys.includes(col || '')) {
        callback(true)

        return
      }

      // We call handsontable predefined validators, if it returns false, we validate as FALSE
      // If return is true, we validate with our custom DQ validations

      // Because of dynamic cell validation, that will change the type of cell to dropdown
      // `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
      if (
        this.editor === 'autocomplete' ||
        this.editor === 'autocomplete.custom'
      ) {
        self
          .getHandsontableValidator('autocomplete')
          .call(this, value, (valid: boolean) => {
            handsontableValid = valid
          })

        if (!handsontableValid) {
          console.warn(
            `HOT Native Validation (autocomplete) - invalid (Value: ${value})`
          )
          callback(false)
          return
        }
      }

      // Now the regular type native HOT validation
      self
        .getHandsontableValidator(colType)
        .call(this, value, (valid: boolean) => {
          handsontableValid = valid
        })

      if (!handsontableValid) {
        console.warn(`HOT Native Validation - invalid (Value: ${value})`)
        callback(false)
        return
      }

      if (!self.lengthCheck(value, self.rules[i].length, colType)) {
        console.warn(`Length Validation - invalid (Value: ${value})`)
        callback(false)
        return
      }

      if (self.isDqCol(col || '')) {
        const dqValid = dqValidate(
          self.getDqDetails(col || ''),
          value,
          colType === 'numeric'
        )

        if (!dqValid) {
          console.warn(`DQ Validation - invalid (Value: ${value})`)
          callback(false)
          return
        }
      }

      callback(true)
    }
  }

  // Validation for length or for size in bytes if col type is numeric
  // True means valid
  private lengthCheck(
    value: any,
    ruleLength: number,
    colType: string
  ): boolean {
    if (isSpecialMissing(value)) return true
    if (value === undefined || value === null) return true
    if (ruleLength === undefined || ruleLength === null) return true

    switch (colType) {
      case 'numeric': {
        if (ruleLength === 8) return true

        if (ruleLength < 3 || ruleLength > 8) {
          console.warn(
            'invalid length from SAS, rule length can only be an integer between 3-8'
          )
          return false
        }

        if (!Number.isInteger(ruleLength)) {
          console.warn(
            'invalid length from SAS, rule length can only be an integer between 3-8'
          )
          return false
        }

        const isFloat = Number(value) === value && value % 1 !== 0
        if (ruleLength < 8 && isFloat) {
          console.warn(
            'reduced length numerics cannot contain decimals else precision will be affected'
          )
          return false
        }

        // values for following cases are taken from https://v8doc.sas.com/sashtml/win/numvar.htm
        switch (ruleLength) {
          case 3:
            return value <= 8192 // 2^13
          case 4:
            return value <= 2097152 // 2^21
          case 5:
            return value <= 536870912 // 2^29
          case 6:
            return value <= 137438953472 // 2^37
          case 7:
            return value <= 35184372088832 // 2^45
        }
      }
      default: {
        return value.toString().length <= ruleLength
      }
    }
  }

  private getHandsontableValidator(type: string) {
    switch (type) {
      case 'autocomplete':
        return autocompleteValidator
      case 'numeric':
        return specialMissingNumericValidator
      case 'intl-date':
        return intlDateValidator
      case 'intl-time':
        return intlTimeValidator
      case 'intl-datetime':
        return datetimeValidator
      default:
        return (value: any, callback: any) => (callback ? callback(true) : null)
    }
  }

  private getRuleIndex(col: string | number): number {
    if (typeof col === 'number') return col

    return this.rules.findIndex((rule: DcValidation) => rule.data === col)
  }

  private setColumnLevelSecurity(index: number) {
    if (!this.sasparams?.CLS_FLAG) return

    const rule = this.rules[index]

    // Primary Key variables must always be visible and read-only
    if (this.primaryKeys.includes(rule.data)) rule.clsRule = 'READ'

    if (rule.clsRule === 'HIDE') this.hiddenColumns.push(index)

    if (rule.clsRule === 'READ') rule.readOnly = true

    if (rule.clsRule === 'EDIT') {
      rule.readOnly = false
      // Guard the -1: splice(-1, 1) drops the LAST hidden column rather than
      // no-op'ing, so an EDIT column that was never hidden would un-hide an
      // unrelated one (e.g. a column hidden by a HIDDEN DQ rule).
      const hiddenIndex = this.hiddenColumns.indexOf(index)
      if (hiddenIndex !== -1) this.hiddenColumns.splice(hiddenIndex, 1)
    }
  }
}

results matching ""

    No results matching ""