src/app/shared/dc-validator/dc-validator.ts
Properties |
|
constructor(sasparams: SASParam, $dataFormats: $DataFormats, cols: Col[], dqRules: DQRule[], dqData: DQData[], hotInstance?: Handsontable)
|
|||||||||||||||||||||
|
Parameters :
|
| Private dqdata |
Type : DQData[]
|
Default value : []
|
| Private dqrules |
Type : DQRule[]
|
Default value : []
|
| 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
|
| 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.
Returns :
boolean
|
| getDqDetails | ||||||||
getDqDetails(col?: string)
|
||||||||
|
DQ Rules values comes from MPE_VALIDATIONS table
Parameters :
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 :
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 :
Returns :
string | undefined
|
| Private getHandsontableValidator | ||||||
getHandsontableValidator(type: string)
|
||||||
|
Parameters :
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 :
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 :
|
| 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 :
Returns :
number | undefined
|
| getRule | ||||||
getRule(selector: string | number)
|
||||||
|
Parameters :
Returns :
DcValidation | undefined
|
| Private getRuleIndex | ||||||
getRuleIndex(col: string | number)
|
||||||
|
Parameters :
Returns :
number
|
| getRules |
getRules()
|
|
Returns :
DcValidation[]
|
| hasDqRules | ||||||||||||||||||||
hasDqRules(col: string, dqrules: DQRuleTypes[], matchAll: boolean)
|
||||||||||||||||||||
|
Checks if the given column has the provided dqrules
Parameters :
Returns :
boolean
|
| isDqCol | ||||||||
isDqCol(col: string)
|
||||||||
|
Checks if given column is or is not included in the current DATA QUALITY rules
Parameters :
Returns :
boolean
|
| Private lengthCheck |
lengthCheck(value: any, ruleLength: number, colType: string)
|
|
Returns :
boolean
|
| registerCustomEditors |
registerCustomEditors()
|
|
Returns :
void
|
| removeRule | ||||||
removeRule(selector: string | number)
|
||||||
|
Parameters :
Returns :
void
|
| Private setColumnLevelSecurity | ||||||
setColumnLevelSecurity(index: number)
|
||||||
|
Parameters :
Returns :
void
|
| Private setDefaultValidator |
setDefaultValidator(self: any, i: number)
|
|
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 :
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)
}
}
}