Index

src/version.ts

adapterPackageFile
Type : unknown
Default value : readFileSync(adapterPackagePath)
adapterPackageFileJson
Type : unknown
Default value : JSON.parse(adapterPackageFile)
adapterPackagePath
Type : unknown
Default value : resolve( __dirname, '..', 'node_modules', '@sasjs', 'adapter', 'package.json' )
file
Type : unknown
Default value : resolve(__dirname, '..', 'src', 'environments', 'version.ts')
gitDescribeSync
Default value : require('git-describe')
gitInfo
Type : unknown
Default value : gitDescribeSync({ dirtyMark: false, dirtySemver: false })
momentObj
Type : unknown
Default value : require('moment')
readFileSync
Default value : require('fs')
relative
Default value : require('path')
resolve
Default value : require('path')
writeFileSync
Default value : require('fs-extra')

src/app/editor/utils/renderers.utils.ts

addDarkClass
Type : unknown
Default value : (td: any) => { if (!td.classList.contains('htDark')) { td.classList.add('htDark') } }

Adds a htDark class to a TD element if not existing

errorRenderer
Type : unknown
Default value : ( instance: any, td: any, row: number, col: number, prop: string, value: any, cellProperties: any ) => { addDarkClass(td) td.innerHTML = `${escapeHtml(value)} <cds-icon shape="exclamation-triangle" status="warning"></cds-icon>` return td }

Custom renderer for HOT cell Used to show error icon

escapeHtml
Type : unknown
Default value : (value: any): string => String(value ?? '').replace(/[&<>"']/g, (char) => { const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' } return entities[char] })

Returns string-safe text of any value so it can be assigned to innerHTML. The cell values painted by the status renderers are user/DB-controlled, so they must never be parsed as HTML by the browser — escaping turns any embedded markup into inert text.

makeNumberFormatRenderer
Type : unknown
Default value : (ruleValue?: string) => { let formatter: Intl.NumberFormat | null = null try { const options = ruleValue ? JSON.parse(ruleValue) : {} formatter = new Intl.NumberFormat(window.navigator.language, options) } catch (e) { console.warn( `NUMBER_FORMAT - invalid Intl.NumberFormat options: ${ruleValue}` ) formatter = null } const baseRenderer = Handsontable.renderers.getRenderer('text') return ( instance: any, td: any, row: number, col: number, prop: string | number, value: any, cellProperties: any ) => { // Render via the base text renderer first to preserve cell styling/classes // (readOnly, alignment, etc.), then override the displayed text. baseRenderer(instance, td, row, col, prop, value, cellProperties) const num = Number(value) if ( formatter && value !== null && value !== undefined && value !== '' && !isNaN(num) ) { td.textContent = formatter.format(num) } return td } }

Builds a display-only HOT renderer that formats numeric cell values using Intl.NumberFormat. The stored/submitted value is never changed — only the rendered text. ruleValue is a JSON string of Intl.NumberFormat options (e.g. '{"style":"currency","currency":"EUR","minimumFractionDigits":2}').

Falls back to the plain text renderer when the JSON is invalid, the options are rejected by Intl.NumberFormat, or the value is not a finite number.

noSpinnerRenderer
Type : unknown
Default value : ( instance: any, td: any, row: number, col: number, prop: string, value: any, cellProperties: any ) => { addDarkClass(td) td.innerHTML = escapeHtml(value) return td }

Custom renderer for HOT cell Used to revert cell back to original state (no spinner, no error)

spinnerRenderer
Type : unknown
Default value : ( instance: any, td: any, row: number, col: number, prop: string, value: any, cellProperties: any ) => { addDarkClass(td) td.innerHTML = `${escapeHtml(value)} <span class="spinner spinner-sm vertical-align-middle"></span>` return td }

Custom renderer for HOT cell Used to show loading spinner in the cell (Spinner shown whilst waiting for SAS to respond)

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

applyFormulaRules
Type : unknown
Default value : ( dataSource: any[], dqRules: DQRule[], columnNames: string[], dataSourceUnchanged: any[], headerPks: string[], userName: string ): any[] => { const formulaRules = dqRules.filter( (rule) => rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA' ) if (formulaRules.length === 0) return dataSource dataSource.forEach((row, rowIndex) => { for (const formulaRule of formulaRules) { const origRow = dataSourceUnchanged.find((candidate) => headerPks.every((pk) => candidate[pk] === row[pk]) ) row[formulaRule.BASE_COL] = parseFormulaRule(formulaRule.RULE_VALUE, { columnNames, rowIndex, userName, origValue: origRow ? origRow[formulaRule.BASE_COL] : undefined }) } }) return dataSource }

Injects the computed =... formula string into each row's data for every HARDFORMULA/SOFTFORMULA column, so Handsontable/HyperFormula displays the evaluated result. HARDFORMULA's readOnly-ness is handled separately in dc-validator.ts, alongside the existing READONLY rule - this only deals with the actual computed value.

Mutates and returns dataSource (same in-place-mutate convention as applyNumericFormats).

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

applyNumericFormats
Type : unknown
Default value : (rules: DcValidation[]): DcValidation[] => { for (let rule of rules) { if (rule.type === 'numeric') { rule.numericFormat = { useGrouping: false, maximumFractionDigits: 20 } rule.locale = window.navigator.language } } return rules }

Applying the numeric formats based on the browser locale/language So that correct decimal separators are applied. For example european format (thousand dot, decimal comma): 1.000,00

Uses Intl.NumberFormat options (HOT 17+) instead of the deprecated numbro pattern/culture. maximumFractionDigits: 20 preserves all natural decimals (Intl's default of 3 would round); locale replaces culture. useGrouping: false keeps raw digits (no thousands separator) - a separator would leak into anything that pattern-matches the displayed value (eg HARDREGEX/SOFTREGEX), and grouping can be opted into per-column with the NUMBER_FORMAT rule.

src/app/shared/utils/hot.utils.ts

baseAfterGetColHeader
Type : unknown
Default value : ( column: number, TH: HTMLTableCellElement, headerLevel: number ) => { // Dark mode TH.classList.add(globals.handsontable.darkTableHeaderClass) }

Function reused in HOT instances to add a class used for dark mode

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

BaseTextEditor
Type : unknown
Default value : Handsontable.editors.getEditor('text') as any
baseTextRenderer
Type : unknown
Default value : Handsontable.renderers.getRenderer('text')
isoPassthrough
Type : unknown
Default value : (value: any): any => value

Identity formatter — display the stored ISO string as-is (no localization).

overrideWithPassthrough
Type : unknown
Default value : (type: string) => { const builtin = Handsontable.cellTypes.getCellType(type) as any Handsontable.cellTypes.registerCellType(type, { ...builtin, valueFormatter: isoPassthrough }) }

Re-registers a built-in HOT cell type (intl-date / intl-time) with only its valueFormatter swapped for the ISO passthrough — the native picker editor and ISO validator are reused from the built-in definition. Idempotent: a second call reads back our already-overridden type, which still carries the original editor/validator.

registerIntlCellTypes
Type : unknown
Default value : () => { overrideWithPassthrough('intl-date') overrideWithPassthrough('intl-time') Handsontable.cellTypes.registerCellType('intl-datetime', { // IntlDatetimeEditor extends the (loosely-typed) text editor returned by // getEditor('text'); cast to satisfy registerCellType's editor signature. editor: IntlDatetimeEditor as any, renderer: baseTextRenderer, validator: datetimeValidator, valueFormatter: isoPassthrough } as any) }

Registers the Intl date/time/datetime cell types. Idempotent — safe to call per DcValidator construction. All three display the stored ISO value verbatim.

SPACE_ISO
Type : unknown
Default value : /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}(?::\d{2})?)$/

src/app/xlmap/utils/file.utils.ts

blobToFile
Type : unknown
Default value : (blob: Blob, fileName: string): File => { const file = new File([blob], fileName, { lastModified: new Date().getTime() }) return file }
byteArrayToBinaryString
Type : unknown
Default value : (res: Uint8Array): string => { // Create a Uint8Array from the input array (if it's not already) const bytes = new Uint8Array(res) // Initialize an empty string to store the binary representation let binary = '' // Get the length of the byte array const length = bytes.byteLength // Iterate through each byte in the array for (let i = 0; i < length; i++) { // Convert each byte to its binary representation and append to the string binary += String.fromCharCode(bytes[i]) } // Return the binary string return binary }

Convert an array of bytes (Uint8Array) to a binary string.

src/app/shared/utils/parse-user-agent.ts

BROWSER_TOKENS
Type : Array<{ name: string; token: RegExp; version: RegExp }>
Default value : [ { name: 'Edge', token: /(?:EdgiOS|Edg[A-Z]?|Edge)\//, version: /(?:EdgiOS|Edg[A-Z]?|Edge)\/([\d.]+)/ }, { name: 'Opera', token: /(?:OPiOS|OPR)\//, version: /(?:OPiOS|OPR)\/([\d.]+)/ }, { name: 'Firefox', token: /(?:FxiOS|Firefox)\//, version: /(?:FxiOS|Firefox)\/([\d.]+)/ }, { name: 'Chrome', token: /(?:CriOS|Chrome)\//, version: /(?:CriOS|Chrome)\/([\d.]+)/ }, // Safari is the fallback token: everything else that reaches here is // WebKit wearing another product's name. { name: 'Safari', token: /Safari\//, version: /Version\/([\d.]+)/ } ]

Most specific token first. Each entry carries the version pattern for the same token, so the family and its version can never be read from different products.

src/app/shared/utils/service-url.ts

buildServiceUrl
Type : unknown
Default value : ({ serverUrl, appLoc, executionPath, service, params = {}, debug = true }: ServiceUrlOptions): string => { // Callers pass the service as the app knows it: `public/getdata` in the // request path, but the request wrapper reports `services/public/getdata` // on a failed request. Both resolve to the same program. const servicePath = service.replace(/^\/+/, '').replace(/^services\//, '') const paramString = Object.entries(params) .filter(([, value]) => value !== null && value !== undefined) .map( ([name, value]) => `&${encodeURIComponent(name)}=${encodeURIComponent(String(value))}` ) .join('') const debugParam = debug ? '&_debug=131' : '' return `${serverUrl}${executionPath}/?_program=${appLoc}/services/${servicePath}${paramString}${debugParam}` }

src/app/editor/utils/classifyRow.ts

classifyRow
Type : unknown
Default value : ( dataRow: any, dataSourceUnchanged: any[], headerPks: string[] ): RowEditStatus => { if (dataRow._____DELETE__THIS__RECORD_____ === 'Yes') return 'D' const dataRowUnchanged = dataSourceUnchanged.find((row: any) => { for (const pkCol of headerPks) { if (row[pkCol] !== dataRow[pkCol]) return false } return true }) if (!dataRowUnchanged) return 'A' // EDIT_STATUS holds this very function's own prior output - comparing it // like any other field would self-perpetuate: once a row is written as // 'M', its EDIT_STATUS would forever differ from the unchanged snapshot's // 'U' even after the real edit is reverted, permanently stuck on 'M'. return JSON.stringify(withoutEditStatus(dataRow)) !== JSON.stringify(withoutEditStatus(dataRowUnchanged)) ? 'M' : 'U' }

Classifies a single row as Modified/Added/Deleted/Unchanged against the pre-edit snapshot (dataSourceUnchanged), PK-matched via headerPks.

Extracted from editor.component.ts's getRowsSubmittingCount(), which used this same logic inline, only at submit time. Kept here as a pure function so it can run live (e.g. on every afterChange, for the EDIT_STATUS column) without duplicating the diffing rules.

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

clearAutoEscaped
Type : unknown
Default value : ( map: AutoEscapedCellMap, rowKey: string, colName: string ): void => { map.get(rowKey)?.delete(colName) }
getRowKey
Type : unknown
Default value : (row: any, headerPks: string[]): string => headerPks.map((pk) => String(row[pk])).join(' ')
isAutoEscaped
Type : unknown
Default value : ( map: AutoEscapedCellMap, rowKey: string, colName: string ): boolean => map.get(rowKey)?.has(colName) ?? false
markAutoEscaped
Type : unknown
Default value : ( map: AutoEscapedCellMap, rowKey: string, colName: string ): void => { if (!map.has(rowKey)) map.set(rowKey, new Set()) map.get(rowKey)!.add(colName) }

src/app/licensing/utils/combinedLicenceKey.ts

COMBINED_KEY_PREFIX
Type : string
Default value : 'DCKEY1:'

Format produced by dckey's own encodeCombinedKey() (main.js): "DCKEY1:" + base64(gzip(licenceKey + " " + activationKey)). The prefix is plain ASCII, not itself compressed/encoded - base64's alphabet never contains ":", so it can never collide with the start of a legacy licence key or activation key, making format detection unambiguous.

gzipDecompress
Type : unknown
Default value : async (bytes: ArrayBuffer): Promise<ArrayBuffer> => { const decompressionStream = new DecompressionStream('gzip') const writer = decompressionStream.writable.getWriter() // Awaited (unlike a fire-and-forget write) so invalid gzip data rejects // through this function's own returned promise, not as a separate // unhandled rejection racing the readable side below. const result = await Promise.all([ writer.write(new Uint8Array(bytes)).then(() => writer.close()), streamToArrayBuffer(decompressionStream.readable) ]) return result[1] }
isCombinedLicenceKey
Type : unknown
Default value : (text: string): boolean => text.trim().startsWith(COMBINED_KEY_PREFIX)
splitCombinedLicenceKey
Type : unknown
Default value : async ( text: string ): Promise<{ licenceKey: string; activationKey: string } | null> => { const trimmed = text.trim() if (!isCombinedLicenceKey(trimmed)) return null const compressedBytes = base64Converter.decode( trimmed.slice(COMBINED_KEY_PREFIX.length) ) const decompressedBytes = await gzipDecompress(compressedBytes) const payload = new TextDecoder().decode(decompressedBytes) const separatorIndex = payload.indexOf(' ') if (separatorIndex === -1) { throw new Error('Invalid combined licence key: missing separator') } return { licenceKey: payload.slice(0, separatorIndex), activationKey: payload.slice(separatorIndex + 1) } }

Splits a combined licence key back into its two parts. Returns null (not a rejected promise) when text isn't a combined key at all, so callers can use it as a "try this, then fall back to the legacy two-field input" check without a try/catch for that common case - a prefixed-but-corrupted string still rejects, since that's a real error, not a format mismatch.

streamToArrayBuffer
Type : unknown
Default value : async ( readable: ReadableStream<Uint8Array> ): Promise<ArrayBuffer> => { const reader = readable.getReader() const chunks: Uint8Array[] = [] let totalLength = 0 while (true) { const { done, value } = await reader.read() if (done) break chunks.push(value) totalLength += value.length } const result = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { result.set(chunk, offset) offset += chunk.length } return result.buffer }

src/app/shared/dc-validator/validations/dq-validation.ts

compareSasNumericOrder
Type : unknown
Default value : ( a: [number, number], b: [number, number] ): number => (a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1])
dqValidate
Type : unknown
Default value : ( dqRules: DQRule[], value: any, isNumeric: boolean = false ): boolean => { for (let detail of dqRules) { if (dqValidation[detail.RULE_TYPE]) { if ( !dqValidation[detail.RULE_TYPE](value, detail.RULE_VALUE, isNumeric) ) { console.warn( `DQ Invalid Reason: ${ detail.RULE_TYPE }\nValue: ${value})\nRule Value: ${ detail.RULE_VALUE.length > 0 && detail.RULE_VALUE !== ' ' ? detail.RULE_VALUE : 'Not defined' }` ) return false } } } return true }
dqValidation
Type : { }
Default value : { CASE: (value: any, ruleValue: string | number): boolean => { switch (ruleValue) { case 'UPCASE': { if ([undefined, null].includes(value)) return true return ( typeof value === 'string' && value.toString() === value.toString().toUpperCase() ) } case 'LOWCASE': { if ([undefined, null].includes(value)) return true return ( typeof value === 'string' && value.toString() === value.toString().toLowerCase() ) } } return true }, MINVAL: (value: any, ruleValue: string | number): boolean => { const valueKey = sasNumericOrderKey(value) const ruleKey = sasNumericOrderKey(ruleValue) // A value that is neither a number nor a missing has no place in the order, // so nothing satisfies the rule. if (!valueKey || !ruleKey) return false return compareSasNumericOrder(valueKey, ruleKey) >= 0 }, MAXVAL: (value: any, ruleValue: string | number): boolean => { const valueKey = sasNumericOrderKey(value) const ruleKey = sasNumericOrderKey(ruleValue) if (!valueKey || !ruleKey) return false return compareSasNumericOrder(valueKey, ruleKey) <= 0 }, NOTNULL: ( value: any, ruleValue: string | number, isNumeric: boolean = false ): boolean => { if (value === undefined || value === null) return false // A special missing (.A-.Z, ._) is NULL as far as a SAS NOT NULL (or // primary key) constraint is concerned - an insert carrying one is // rejected with an integrity constraint error - so the rule must reject // it too, or the editor would accept a value the target table refuses. // Numeric columns only: a lone letter is ordinary data on a character // column. if (isNumeric && isSpecialMissing(value)) return false return value.toString().length > 0 }, // Pattern is used as authored, not auto-anchored — a rule author who // wants a full-value match must write ^...$ themselves. HARDREGEX: ( value: any, ruleValue: string | number, isNumeric: boolean = false ): boolean => { if (isRegexRuleExempt(value, isNumeric)) return true try { return parseRegexRule(ruleValue.toString()).test(value.toString()) } catch (e) { console.warn( `HARDREGEX - invalid pattern, treated as always-valid: ${ruleValue}` ) return true } } }
sasNumericOrderKey
Type : unknown
Default value : (value: any): [number, number] | null => { if (value === undefined || value === null || value === '') return [0, 1] // regular missing if (typeof value === 'string') { const upper = value.trim().toUpperCase() if (upper === '.' || upper === '') return [0, 1] // regular missing if (upper === '._' || upper === '_') return [0, 0] // the lowest missing if (/^\.?[A-Z]$/.test(upper)) return [0, 2 + (upper.charCodeAt(upper.length - 1) - 65)] // .A .. .Z const numValue = parseFloat(upper) return isNaN(numValue) ? null : [1, numValue] } const numValue = Number(value) return isNaN(numValue) ? null : [1, numValue] }

A SAS numeric variable's values have a total order, and its missing values sit below every non-missing value. The missing values are themselves ordered: ._ is the lowest, then the regular missing, then .A through .Z.

A range rule compares in that order. That is what makes a range of missings meaningful - with MINVAL .A and MAXVAL .C, .B is inside the range and .D is outside it - and it is also why a special missing fails a numeric floor: it sorts below every number.

The key is a pair: the class (0 for a missing, 1 for a number, so that every number sorts above every missing) and the position within that class. A value that is neither a number nor a missing has no place in the order and gets null, which no rule accepts.

src/app/editor/utils/regex-warning-renderer.ts

compileRegex
Type : unknown
Default value : ( pattern: string | undefined, ruleType: 'SOFTREGEX' | 'HARDREGEX' ): RegExp | null => { try { return pattern ? parseRegexRule(pattern) : null } catch (e) { console.warn(`${ruleType} - invalid pattern, warning disabled: ${pattern}`) return null } }
makeRegexWarningRenderer
Type : unknown
Default value : ( softPattern?: string, hardPattern?: string, isNumeric: boolean = false ) => { const hardRegex = compileRegex(hardPattern, 'HARDREGEX') const softRegex = hardRegex ? null : compileRegex(softPattern, 'SOFTREGEX') const baseRenderer = Handsontable.renderers.getRenderer('text') return ( instance: any, td: any, row: number, col: number, prop: string | number, value: any, cellProperties: any ) => { baseRenderer(instance, td, row, col, prop, value, cellProperties) const markedForDelete = instance.getDataAtRowProp(row, '_____DELETE__THIS__RECORD_____') === 'Yes' const exempt = isRegexRuleExempt(value, isNumeric) const failsHard = !!hardRegex && !exempt && !hardRegex.test(value.toString()) const failsSoft = !!softRegex && !exempt && !softRegex.test(value.toString()) if (markedForDelete) return td if (failsHard) { td.title = `REGEX: ${hardPattern}` } else if (failsSoft) { td.classList.add('dc-warning-cell') td.title = `REGEX: ${softPattern}` } return td } }

Builds a display-only HOT renderer for HARDREGEX/SOFTREGEX: neither ever returns false from the cell validator here (that's dqValidate's job for HARDREGEX, which blocks submission and paints HOT's own red htInvalid independently of this renderer) - this renderer only adds the matching REGEX: <pattern> title, plus a yellow dc-warning-cell class when only SOFTREGEX fails, same split as makeNumberFormatRenderer.

Only one regex ever runs per column: when a HARDREGEX rule exists, SOFTREGEX is ignored entirely (never compiled, never evaluated) - regardless of whether individual cell values pass or fail the hard rule. A value failing HARDREGEX gets its title (no yellow class - red htInvalid already covers the color). This mirrors DcValidator.failsSoftRegex's own precedence.

Suppressed on rows marked for delete (DELETE__THIS__RECORD = 'Yes') — a warning about data about to be removed is just noise.

Falls back to no warning ever showing when a pattern itself is malformed, rather than breaking the cell.

src/app/editor/utils/date.utils.ts

dateFormat
Type : unknown
Default value : (date: Date) => { return ( date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2) ) }

Converts date object to the YYYY-MM-DD

dateToTime
Type : unknown
Default value : (date: Date) => { let timeStr = ('0' + date.getHours()).slice(-2) + ':' timeStr = timeStr + ('0' + date.getMinutes()).slice(-2) + ':' timeStr = timeStr + ('0' + date.getSeconds()).slice(-2) return timeStr }

Converts date object to the time string

dateToUtcTime
Type : unknown
Default value : (date: Date) => { let timeStr = ('0' + date.getUTCHours()).slice(-2) + ':' timeStr = timeStr + ('0' + date.getUTCMinutes()).slice(-2) + ':' timeStr = timeStr + ('0' + date.getUTCSeconds()).slice(-2) return timeStr }

Converting date object to the UTC time string

padTimeString
Type : unknown
Default value : (value: string): string => { const match = value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/) if (!match) return value const [, hours, minutes, seconds] = match return `${hours.padStart(2, '0')}:${minutes}:${seconds ?? '00'}` }

Normalises a 24-hour time STRING to zero-padded HH:mm:ss.

The intl-time cell type (validator + native <input type=time> editor) is strict and rejects H:mm[:ss], so such a value paints red and the editor opens empty. Pad the hour and default the seconds so the value validates.

Non time-like input is returned unchanged.

src/app/shared/dc-validator/validations/hot-custom-validators.ts

DATETIME_REGEX
Type : unknown
Default value : /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/

src/app/services/sas.service.ts

DEBUG_STATE_KEY
Type : string
Default value : 'dc-debug-state'

Where the debug toggle state is kept. The <sasjs> tag in index.html is re-read on every load, so without this a refresh would drop the state the user set from the UI.

DIAGNOSTICS_SERVICES
Type : []
Default value : [ 'services/public/startupservice', 'services/editors/getdata', 'services/editors/getdynamiccolvals', 'services/editors/stagedata', 'services/editors/loadfile', 'services/editors/restore', 'services/auditors/postdata' ]

Services that receive the browser_info / browser_url_vars diagnostics tables: the startup service (so every session logs its context once) and every service that executes customer-provided code - hook scripts via %mpe_runhook (getdata, stagedata, loadfile, restore, postdata) and the dynamic cell dropdown programs (getdynamiccolvals). Other services never see the tables, so the payload stays off the high-frequency calls.

src/app/services/utils/decodeLicenceFeatures.ts

decodeFeaturesObject
Type : unknown
Default value : ( features: LicenceFeaturesObject ): Partial<LicenceState> => ({ viewer_rows_allowed: features.vra ?? Infinity, editor_rows_allowed: features.era ?? Infinity, stage_rows_allowed: features.sra ?? Infinity, history_rows_allowed: features.hra ?? Infinity, submit_rows_limit: features.srl ?? Infinity, tables_in_library_limit: features.till ?? Infinity, viewbox: features.vb, viewbox_limit: features.vbl ?? Infinity, lineage_daily_limit: features.ldl ?? Infinity, fileUpload: features.fu, editRecord: features.er, addRecord: features.ar })
decodeFeaturesString
Type : unknown
Default value : (features: string): Partial<LicenceState> => { const featuresMap = features.split(',') const parseValue = (codeBit: string) => codeBit === '-' ? Infinity : parseInt(codeBit) const parseToggle = (codeBit: string) => !!parseInt(codeBit) return { viewer_rows_allowed: parseValue( featuresMap[LicenceFeaturesMap.viewer_rows_allowed] ), editor_rows_allowed: parseValue( featuresMap[LicenceFeaturesMap.editor_rows_allowed] ), stage_rows_allowed: parseValue( featuresMap[LicenceFeaturesMap.stage_rows_allowed] ), history_rows_allowed: parseValue( featuresMap[LicenceFeaturesMap.history_rows_allowed] ), submit_rows_limit: parseValue( featuresMap[LicenceFeaturesMap.submit_rows_limit] ), tables_in_library_limit: parseValue( featuresMap[LicenceFeaturesMap.tables_in_library_limit] ), viewbox_limit: parseValue(featuresMap[LicenceFeaturesMap.viewbox_limit]), lineage_daily_limit: parseValue( featuresMap[LicenceFeaturesMap.lineage_daily_limit] ), viewbox: parseToggle(featuresMap[LicenceFeaturesMap.viewbox]), fileUpload: parseToggle(featuresMap[LicenceFeaturesMap.fileUpload]), editRecord: parseToggle(featuresMap[LicenceFeaturesMap.editRecord]), addRecord: parseToggle(featuresMap[LicenceFeaturesMap.addRecord]) } }
decodeLicenceFeatures
Type : unknown
Default value : ( features: string | LicenceFeaturesObject ): Partial<LicenceState> => { if (typeof features === 'object') { return decodeFeaturesObject(features) } return decodeFeaturesString(features) }

src/app/services/licence.service.ts

DEFAULT_HOT_LICENCE_KEY
Type : string
Default value : 'non-commercial-and-evaluation'

Handsontable licence key used until a licence is loaded from the backend. The decrypted licence carries the customer's own key (hot_license_key), so this is only the pre-licence / free-tier default - it used to be supplied by the <sasjs> tag.

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

DEFAULT_VALUE_RULE_TYPES
Type : DQRuleTypes[]
Default value : [ 'NOTNULL', 'READONLY', 'HIDDEN' ]

Rule types whose RULE_VALUE supplies the default value inserted into a cell when a new row is added. Checked in priority order.

src/app/shared/utils/retry-once.ts

defaultWait
Type : unknown
Default value : (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
retryOnce
Type : unknown
Default value : async <T>( requestFn: () => Promise<T>, options?: RetryOnceOptions ): Promise<T> => { const maxDelayMs = options?.maxDelayMs ?? 3000 const random = options?.random ?? Math.random const wait = options?.wait ?? defaultWait try { return await requestFn() } catch { await wait(random() * maxDelayMs) return requestFn() } }

Runs requestFn; if it rejects, waits a random 0..maxDelayMs and tries exactly once more. A second rejection propagates as-is. Multiple concurrent DC instances (e.g. VA multi-page reports) can cause the startup request to time out even though a second attempt succeeds.

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

DELETE_RECORD_COLUMN_LABEL
Type : string
Default value : 'Delete?'

Display label editor.component.ts renames the delete-flag column's headerColumns entry to (see initSetup) - the actual Handsontable column data prop stays DELETE_RECORD_COLUMN_RULE.data, so by the time headerColumns reaches this function the raw name is already gone from it and only this label is left to match against.

getRevertableCols
Type : unknown
Default value : ( dqRules: DQRule[], headerColumns: string[] ): string[] => { const stableFormulaBaseCols = new Set(getStableFormulaBaseCols(dqRules)) const unstableFormulaBaseCols = new Set( dqRules .filter( (rule) => (rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA') && !stableFormulaBaseCols.has(rule.BASE_COL) ) .map((rule) => rule.BASE_COL) ) return headerColumns.filter( (col) => col !== DELETE_RECORD_COLUMN_RULE.data && col !== DELETE_RECORD_COLUMN_LABEL && col !== EDIT_STATUS_COLUMN_NAME && !unstableFormulaBaseCols.has(col) ) }

Every column eligible to be checked/marked as "overwritten" (see findOverwrittenCells) - all of headerColumns except the delete-flag column, the hidden EDIT_STATUS column, and any HARDFORMULA/SOFTFORMULA column excluded by getStableFormulaBaseCols (DC.USER_NAME/DC.ORIG_VALUE/ DC.ROW_STATUS-referencing formulas are inherently session-dependent, so comparing their live result against the raw SAS value is never a meaningful "was this overwritten" signal).

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

DELETE_RECORD_COLUMN_NAME
Type : string
Default value : '_____DELETE__THIS__RECORD_____'

_____DELETE__THIS__RECORD_____ is a purely client-owned column - the delete checkbox column. getdata.sas does NOT emit it: the editor adds the name to COLHEADERS itself (see editor.component.ts's initSetup) and seeds every row to 'No'. A response that carries it is refused, because the name can then only come from a real table column of that name (or a hook that re-added it), which would silently shadow the delete flag.

Its rule can never travel via cols[].COLTYPE - %mp_getcols (which builds cols1, the left side of the join into cols) has no row for it - so it is injected directly, here and in DcValidator's constructor.

DELETE_RECORD_COLUMN_RULE
Type : DcValidation
Default value : { data: DELETE_RECORD_COLUMN_NAME, type: 'dropdown', source: ['No', 'Yes'] }

src/app/licensing/utils/detectLicenceKeyProtocolMismatch.ts

detectLicenceKeyProtocolMismatch
Type : unknown
Default value : ( licenceKey: string, activationKey: string, isSecureContext: boolean ): LicenceKeyProtocolMismatch => { const trimmedLicenceKey = licenceKey.trim() const trimmedActivationKey = activationKey.trim() if (!trimmedLicenceKey || !trimmedActivationKey) return null const isHttpsFormatKey = trimmedLicenceKey !== trimmedActivationKey if (isHttpsFormatKey && !isSecureContext) return 'requiresHttps' if (!isHttpsFormatKey && isSecureContext) return 'requiresHttp' return null }

Detects a licence key generated for one protocol (http/https) being pasted/uploaded on a page where the other one is actually in effect - before any decrypt attempt, purely from the raw key text.

A key generated for an insecure connection always carries the same text in both the licence key and activation key fields; a key generated for a secure connection always carries two different values. That distinction alone tells us which format a pasted key is, without needing to attempt decryption first.

This is a structural assumption about key-generation logic (a separate repo) - if that ever changes to generate distinct values for HTTP-format keys too, this detection would silently misclassify keys.

isSecureContext must reflect whether this browsing context can actually decrypt a secure-connection key (mirrors the check licence.service.ts's own decryptLicenseKey() makes) - not simply location.protocol === 'https:'. Browsers treat localhost as a secure context even over plain http, so a key meant for an insecure connection still fails to decrypt there, exactly like it would on a real https page.

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

EDIT_STATUS_COLUMN_NAME
Type : string
Default value : 'dc.row_status'

EDIT_STATUS is a purely client-synthesized column (M/A/D/U from classifyRow), added so DC.ROW_STATUS has a real, HyperFormula-addressable cell to point at. It never comes from COLHEADERS, is always hidden (see DcValidator's constructor) and is stripped before the submit payload leaves the browser (see editor.component.ts's saveTable()).

Named with a period rather than decorated with underscores (like DELETE_RECORD_COLUMN_RULE) - a SAS variable name can never contain a period, so this makes a collision with a real dataset column structurally impossible, not just unlikely. Handsontable's own datamap.get()/set() only treat a dot in a data key as a nested-path separator when the row has no OWN flat property of that exact name - since every row always gets this key from hotDataSchema (a flat copy, dots included, see DataMap.createRow's deepExtend), that fallback never triggers here.

EDIT_STATUS_COLUMN_RULE
Type : DcValidation
Default value : { data: EDIT_STATUS_COLUMN_NAME, type: 'text', readOnly: true }

src/environments/environment.prod.ts

environment
Type : object
Default value : { production: true, commitId: 'to be updated' // process.env.CI_COMMIT_ID }

src/environments/environment.ts

environment
Type : object
Default value : { production: false, commitId: 'to be updated' // process.env.CI_COMMIT_ID }

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

escapeCharacterColumnValue
Type : unknown
Default value : ( value: unknown ): { value: unknown; wasEscaped: boolean } => { if (typeof value !== 'string' || !value.startsWith('=')) { return { value, wasEscaped: false } } return { value: `'${value}`, wasEscaped: true } }

Prepends ' to a =-led value so Handsontable/HyperFormula treats it as inert text rather than a live formula - Handsontable's own escape convention, which it strips automatically when displaying the value. Caller decides eligibility (character column? not a formula base column?) before calling this - it only ever looks at the value itself.

src/app/shared/utils/col-info-html.ts

escapeHtml
Type : unknown
Default value : (value: any): string => String(value ?? '').replace(/[&<>"']/g, (char) => { const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' } return entities[char] })

Returns string-safe text of any value so it can be concatenated into a string that is later assigned to raw DOM innerHTML. The column metadata (label/format) and DQ RULE_VALUE strings (regex/formula) are DB-controlled - a validation-rule author can store markup such as <img src=x onerror=...> in a HARDREGEX value or a column label - so they must never be parsed as HTML by the browser. Escaping turns any embedded markup into inert text.

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

escapeRegExpMetacharacters
Type : unknown
Default value : (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
parseFormulaRule
Type : unknown
Default value : ( ruleValue: string, context: FormulaVariableContext ): string => { // A leading '=' is the rule author's own responsibility- it's not // inserted here. It's stripped before // substitution and re-attached after so substituteBoundedToken's // start-of-string boundary check still lines up with the first real // token, exactly as if it had never been there. const hasLeadingEquals = ruleValue.startsWith('=') const formulaBody = hasLeadingEquals ? ruleValue.slice(1) : ruleValue const quotedSpanPattern = /"[^"]*"|'[^']*'/g let result = '' let lastIndex = 0 let match: RegExpExecArray | null const substituteUnquotedSpan = (span: string): string => { let substituted = substituteBoundedToken( span, 'DC.USER_NAME', quoteLiteral(context.userName) ) substituted = substituteBoundedToken( substituted, 'DC.ORIG_VALUE', quoteLiteral(context.origValue) ) // Unlike DC.USER_NAME/DC.ORIG_VALUE, DC.ROW_STATUS resolves to a real // cell reference (not a quoted literal) - it points at the EDIT_STATUS // column, which holds this row's live M/A/D/U classification, so other // formulas can react to it, e.g. `=if(A1!='U',"sasdemo","sasinstaller")`. const rowStatusColIndex = context.columnNames.indexOf( EDIT_STATUS_COLUMN_NAME ) if (rowStatusColIndex !== -1) { const rowStatusCellRef = `${Handsontable.helper.spreadsheetColumnLabel(rowStatusColIndex)}${context.rowIndex + 1}` substituted = substituteBoundedToken( substituted, 'DC.ROW_STATUS', rowStatusCellRef ) } context.columnNames.forEach((columnName, columnIndex) => { const cellRef = `${Handsontable.helper.spreadsheetColumnLabel(columnIndex)}${context.rowIndex + 1}` substituted = substituteBoundedToken(substituted, columnName, cellRef) }) return substituted } while ((match = quotedSpanPattern.exec(formulaBody))) { result += substituteUnquotedSpan(formulaBody.slice(lastIndex, match.index)) result += match[0] lastIndex = match.index + match[0].length } result += substituteUnquotedSpan(formulaBody.slice(lastIndex)) return hasLeadingEquals ? `=${result}` : result }
quoteLiteral
Type : unknown
Default value : (value: string | number | undefined): string => `"${String(value ?? '').replace(/"/g, '""')}"`
substituteBoundedToken
Type : unknown
Default value : ( text: string, token: string, replacement: string ): string => { const pattern = new RegExp( `(?<=^|\\s)${escapeRegExpMetacharacters(token)}(?=$|\\s)`, 'g' ) // A function replacement's return value is used verbatim - a string // replacement would otherwise have String.replace interpret $&/$`/$'/ // $<digit> specially, and `replacement` here can be arbitrary data (a // cell's prior value, a username) that may legitimately contain '$'. return text.replace(pattern, () => replacement) }

Replaces token with replacement wherever it has a leading and trailing blank (a literal space, or the start/end of this span). This lets PRICE * VOLUME substitute correctly while MATCH(PRICE) (no surrounding blanks) does not, without needing to know anything about function names.

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

escapeRegExpMetacharacters
Type : unknown
Default value : (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

HARDREGEX/SOFTREGEX rule values are authored as SAS PRX patterns, which require the delimiter form /pattern/flags (prxparse's own syntax check in mpe_validations_postedit.sas requires it). A bare pattern with no delimiters is also accepted, for backwards compatibility with existing rules/tests. Passing the delimited form as-is to new RegExp(string) would treat the slashes and flags as literal pattern text instead of stripping/applying them, so this parses the PRX form before constructing the regex.

PRX also accepts some Perl-only regex syntax with no direct JS equivalent. Three mechanical, unambiguous cases are translated below - a leading (?i) modifier, an escaped \Q...\E literal sequence, and \A/\z absolute anchors. Atomic groups ((?>...)) and possessive quantifiers (a++) are NOT translated: both would need a lookahead+backreference rewrite that changes the pattern's capture-group numbering, a real correctness risk for comparatively rare constructs, so they're left to fail (they already throw, which every caller already treats as "always valid" / "never warn", same as any other malformed pattern).

parseRegexRule
Type : unknown
Default value : (ruleValue: string): RegExp => { const delimited = /^\/(.*)\/([a-z]*)$/s.exec(ruleValue) if (!delimited) return new RegExp(ruleValue) let [, body, flags] = delimited const inlineCaseInsensitive = /^\(\?i\)/.exec(body) if (inlineCaseInsensitive) { body = body.slice(inlineCaseInsensitive[0].length) if (!flags.includes('i')) flags += 'i' } body = body.replace(/\\Q([\s\S]*?)\\E/g, (_match, literal) => escapeRegExpMetacharacters(literal) ) body = body.replace(/\\A/g, '^').replace(/\\z/g, '(?![\\s\\S])') return new RegExp(body, flags) }

src/environments/_eula.ts

EULA
Type : unknown
Default value : ` Licence Agreement for Data Controller for SAS® ==================== Copyright (c) Bowe IO Ltd Data Controller software is distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered at Companies House with company number 08777171, VAT number: 203914240 This software is protected by applicable copyright laws, including international treaties, and dual-licensed – depending on whether your use for commercial purposes, meaning intended for or resulting in commercial advantage or monetary compensation, or not. If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing the suitability, performance, and usefulness of this software outside the production environment, you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file available here: https://git.datacontroller.io/dc/dc/src/branch/main/licence-non-commercial-datacontroller.md Your use of this software for commercial purposes is subject to the terms included in an applicable license agreement. In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software. UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE. `

src/app/editor/utils/preventMenuItemAutoClose.ts

EVENTS_TO_ISOLATE
Type : unknown
Default value : [ 'mousedown', 'mouseup', 'contextmenu', 'selectstart' ] as const
preventMenuItemAutoClose
Type : unknown
Default value : (elem: HTMLElement): void => { for (const eventName of EVENTS_TO_ISOLATE) { elem.addEventListener(eventName, (event) => event.stopPropagation()) } }

src/app/editor/utils/grid.utils.ts

excelDateToJSDate
Type : unknown
Default value : (serial: number) => { return new Date(Math.round((serial - 25569) * 86400 * 1000)) }

Converts excel date serial number to JS date

getMissingHeaders
Type : unknown
Default value : (data: any, headers: any) => { const missingHeaders: string[] = [] const remainingHeaders: string[] = [] headers.forEach((element: string) => { if (data.indexOf(element) === -1) { missingHeaders.push(element) } else { remainingHeaders.push(element) } }) return [missingHeaders, remainingHeaders] }

Captures headers that are not found in the current table but is found in the uploaded file data

parseTableColumns
Type : unknown
Default value : (data: Col[]): string[] => { const columns: string[] = [] for (let specEntry of data) { if (specEntry.NAME !== '_____DELETE__THIS__RECORD_____') { columns.push(specEntry.NAME) } } return columns }

Parsing table columns for the HOT in editor Converts array of objects into array of strings, every string is column name (key)

src/app/editor/utils/expandCellRanges.ts

expandCellRanges
Type : unknown
Default value : ( ranges: SimpleCellRange[], totalRows: number, totalCols: number ): SimpleCell[] => { const cells: SimpleCell[] = [] const normalizeRow = (row: number | null) => row === -1 || row === null ? undefined : row const normalizeCol = (col: number | null) => col === -1 || col === null ? undefined : col for (const range of ranges) { const rowA = normalizeRow(range.from.row) ?? 0 const rowB = normalizeRow(range.to.row) ?? totalRows - 1 const colA = normalizeCol(range.from.col) ?? 0 const colB = normalizeCol(range.to.col) ?? totalCols - 1 const startRow = Math.min(rowA, rowB) const endRow = Math.max(rowA, rowB) const startCol = Math.min(colA, colB) const endCol = Math.max(colA, colB) for (let row = startRow; row <= endRow; row++) { for (let col = startCol; col <= endCol; col++) { cells.push({ row, col }) } } } return cells }

Expands one or more Handsontable selection ranges (as returned by getSelectedRange()/the context menu callback's selection argument) into every individual (row, col) cell they cover. A whole-row selection (row header click) reports col: -1 (Handsontable.CellRange itself types this as number | null, so null is treated the same way here) on whichever end is the "start" of the range; a whole-column selection reports row: -1 the same way - normalized here to the real 0..totalRows-1/0..totalCols-1 bounds, since neither -1 nor null is a usable index for anything downstream (comments lookup, setDataAtRowProp, ...). Multiple ranges (ctrl-click) are all expanded, not just the first.

src/app/xlmap/utils/xl.utils.ts

extractRowAndCol
Type : unknown
Default value : (str: string) => { // Regular expression to match and capture the values inside square brackets const regex = /R\[(\d+)\]C\[(\d+)\]/ // Match the regular expression against the input string const match = str.match(regex) if (!match) { return null } // Extract values from the match groups const row = parseInt(match[1], 10) const column = parseInt(match[2], 10) return { row, column } }

Extracts row and column number from xlmap rule.

Input string should be in form of either "MATCH F R[2]C[0]: CASH BALANCE" or "RELATIVE R[10]C[6]"

getCellAddress
Type : unknown
Default value : (rule: string, arrayOfObjects: any[]) => { if (rule.startsWith('ABSOLUTE ')) { rule = rule.replace('ABSOLUTE ', '') } if (rule.startsWith('RELATIVE ')) { const rowAndCol = extractRowAndCol(rule) if (rowAndCol) { const { row, column } = rowAndCol // Generate an A1-Style address string from a SheetJS cell address // Spreadsheet applications typically display ordinal row numbers, // where 1 is the first row, 2 is the second row, etc. The numbering starts at 1. // SheetJS follows JavaScript counting conventions, // where 0 is the first row, 1 is the second row, etc. The numbering starts at 0. // Therefore, we have to subtract 1 from row and column to match SheetJS indexing convention rule = XLSX.utils.encode_cell({ r: row - 1, c: column - 1 }) } } if (rule.startsWith('MATCH ')) { let targetValue = '' // using a regular expression to match "C[x]:" and extract the value after it const match = rule.match(/C\[\d+\]:(.+)/) // Check if there is a match if (match) { // Extract the value after "C[x]:" targetValue = match[1] } // Split the string by spaces to get target row/column const splittedArray = rule.split(' ') // Extract the second word const secondWord = splittedArray[1] let targetColumn = '' let targetRow = -1 let cellAddress = '' // Check if the secondWord is a number if (!isNaN(Number(secondWord))) { targetRow = parseInt(secondWord) } else { targetColumn = secondWord } if (targetRow !== -1) { // sheetJS index starts from 0, // therefore, decremented 1 to make it correct row address for js array const row = arrayOfObjects[targetRow - 1] for (const col in row) { if (col !== '__rowNum__' && row[col] === targetValue) { cellAddress = col + targetRow break } } } else { for (let i = 0; i < arrayOfObjects.length; i++) { const row = arrayOfObjects[i] if (row[targetColumn] === targetValue) { // sheetJS index starts from 0, // therefore, incremented 1 to make it correct row address const rowIndex = i + 1 cellAddress = targetColumn + rowIndex break } } } // Converts A1 cell address to 0-indexed form const matchedCellAddress = XLSX.utils.decode_cell(cellAddress) // extract number of rows and columns that we have to move // from matched cell to reach target cell const rowAndCol = extractRowAndCol(rule) if (rowAndCol) { const { row, column } = rowAndCol // Converts 0-indexed cell address to A1 form rule = XLSX.utils.encode_cell({ r: matchedCellAddress.r + row, c: matchedCellAddress.c + column }) } } return rule }

Generate an A1-Style excel cell address from xlmap rule.

Expect "ABSOLUTE D8" or "RELATIVE R[10]C[6]" or "MATCH C R[0]C[4]:Common Equity Tier 1 (CET1)" kinds of string as rule input

getFinishingCell
Type : unknown
Default value : ( start: string, finish: string, arrayOfObjects: any[] ) => { // in this case an individual cell would be extracted if (finish === '') { return start } if (finish.startsWith('ABSOLUTE ')) { finish = finish.replace('ABSOLUTE ', '') } if (finish.startsWith('RELATIVE ')) { const rowAndCol = extractRowAndCol(finish) if (rowAndCol) { const { row, column } = rowAndCol const { r, c } = XLSX.utils.decode_cell(start) // finish is relative to starting point // therefore, we need to add extracted row and columns // in starting cell address to get actual finishing cell finish = XLSX.utils.encode_cell({ r: r + row, c: c + column }) } } if (finish.startsWith('MATCH ')) { finish = getCellAddress(finish, arrayOfObjects) } if (finish === 'LASTDOWN') { const { r, c } = XLSX.utils.decode_cell(start) const colName = XLSX.utils.encode_col(c) let lastNonBlank = r for (let i = r + 1; i < arrayOfObjects.length; i++) { const row = arrayOfObjects[i] if (!row[colName]) { break } lastNonBlank = i } finish = colName + (lastNonBlank + 1) // excel numbering starts from 1. So incremented 1 to 0 based index } if (finish === 'BLANKROW') { const { r } = XLSX.utils.decode_cell(start) let lastNonBlankRow = r for (let i = r + 1; i < arrayOfObjects.length; i++) { const row = arrayOfObjects[i] if (isBlankRow(row)) { break } lastNonBlankRow = i } const row = arrayOfObjects[lastNonBlankRow] // Get the keys of the object (excluding '__rowNum__') const keys = Object.keys(row).filter((key) => key !== '__rowNum__') // Finding last column in a row // Find the key with the highest alphanumeric value (assumes keys are letters) const lastColumn = keys.reduce( (maxKey, currentKey) => (currentKey > maxKey ? currentKey : maxKey), '' ) // make finishing cell address in A1 style finish = lastColumn + (lastNonBlankRow + 1) // excel numbering starts from 1. So incremented 1 to 0 based index } return finish }

Generate an A1-Style excel cell address for last cell

isBlankRow
Type : unknown
Default value : (row: any) => { for (const key in row) { if (key !== '__rowNum__') { return false } } return true }

Checks if an excel row is blank or not

src/eula.ts

file
Type : unknown
Default value : resolve(__dirname, '..', 'src', 'environments', '_eula.ts')
licenceFilePath
Type : unknown
Default value : resolve(__dirname, '..', '..', 'LICENCE.md')
readFileSync
Default value : require('fs')

Recreate _eula.ts based on LICENCE.md an put it in environments folder so it can be used in the angular components

relative
Default value : require('path')
resolve
Default value : require('path')
writeFileSync
Default value : require('fs-extra')

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

findOverwrittenCells
Type : unknown
Default value : ( currentRows: Record<string, unknown>[], rawRows: Record<string, unknown>[], revertableCols: string[], headerPks: string[] ): OverwrittenCell[] => { const changes: OverwrittenCell[] = [] currentRows.forEach((row, rowIndex) => { const rawRow = rawRows.find((candidate) => headerPks.every((pk) => candidate[pk] === row[pk]) ) if (!rawRow) return for (const col of revertableCols) { const rawValue = rawRow[col] if (rawValue === undefined || rawValue === null || rawValue === '') { continue } const currentValue = row[col] if (String(rawValue) === String(currentValue)) continue changes.push({ rowIndex, col, originalValue: rawValue }) } }) return changes }

Finds every (row, column) pair where the current value differs from the real, raw value SAS actually sent for it - i.e. something (a direct edit, paste, a formula, ...) silently changed a value that pre-existed in the actual data, not just filled in a blank. Rows are matched by primary key, not array position, so detection stays correct across row insert/delete/ sort. A row with no PK match in rawRows (a newly-inserted row) is skipped entirely, since there's no original value to have overwritten. Compared loosely (via string coercion) since the raw value arrives as whatever type SAS sent while the current value may be a different but equal representation (e.g. HyperFormula's own numeric result).

src/app/free-tier.config.ts

freeTierConfig
Type : LicenceState
Default value : { users_allowed: 1, viewer_rows_allowed: 15, editor_rows_allowed: 15, stage_rows_allowed: Infinity, history_rows_allowed: 15, submit_rows_limit: 5, viewbox_limit: 1, lineage_daily_limit: 3, tables_in_library_limit: 35, viewbox: true, fileUpload: false, editRecord: true, addRecord: true }

These properties Will be applied when NO KEY FREE TIER is active and when key is FREE TIER it is used for limit values that are not set props from the LICENCE KEY is will be used to override these values

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

getDqDataCols
Type : unknown
Default value : (dqData: DQData[]): string[] => { const cols: string[] = [] dqData.forEach((element: DQData) => { if (!cols.includes(element.BASE_COL)) cols.push(element.BASE_COL) }) return cols }

src/app/editor/utils/getEditStatusSymbol.ts

getEditStatusSymbol
Type : unknown
Default value : (status: RowEditStatus): string => { switch (status) { case 'A': return '+' case 'D': return '-' case 'M': return '±' case 'U': return '' } }

Maps a row's edit status to the symbol shown in the row-header gutter (see editor.component.ts's rowHeaders callback). Unchanged renders as an empty string rather than a letter, since it's the common case and would otherwise clutter every untouched row.

src/app/licensing/utils/getEnabledFeatureLabels.ts

getEnabledFeatureLabels
Type : unknown
Default value : ( features: string | LicenceFeaturesObject | undefined ): string[] => { if (!features) return [] const decoded = decodeLicenceFeatures(features) return TOGGLE_FEATURE_LABELS.filter(({ key }) => decoded[key]).map( ({ label }) => label ) }
TOGGLE_FEATURE_LABELS
Type : literal type[]
Default value : [ { key: 'viewbox', label: 'Viewbox' }, { key: 'fileUpload', label: 'File Upload' }, { key: 'editRecord', label: 'Edit Record' }, { key: 'addRecord', label: 'Add Record' } ]

src/app/query/utils/getFilterObjPath.ts

getFilterObjPath
Type : unknown
Default value : ( rootParam: string, viewboxId?: number ): string => { if (viewboxId) return `viewboxes.${viewboxId}` if (rootParam === 'home' || rootParam === 'editor') return 'editor' if (rootParam === 'view') return 'viewer' return '' }

Resolves which bucket of the globals filter cache (_globals.ts) a QueryComponent instance should read/write. viewboxId is checked first, unconditionally.

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

getFormulaCellsToPreserveOnCancel
Type : unknown
Default value : ( rowCount: number, formulaBaseCols: string[], hasComment: (rowIndex: number, baseCol: string) => boolean, getComputedValue: (rowIndex: number, prop: string) => unknown ): PreservedFormulaCell[] => { const preserved: PreservedFormulaCell[] = [] for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { for (const baseCol of formulaBaseCols) { if (!hasComment(rowIndex, baseCol)) continue preserved.push({ rowIndex, prop: baseCol, value: getComputedValue(rowIndex, baseCol) }) } } return preserved }

Cells that must keep their LIVE computed value when an edit session is cancelled, rather than being blindly overwritten by dataSourceUnchanged's raw baseline. dataSourceUnchanged deliberately holds the raw pre-formula value for a HARDFORMULA/SOFTFORMULA column whenever it still has its markFormulaChangedCells comment (see editTable's overlay) - that's needed so classifyRow flags the row as modified, but it isn't a session edit to discard: the formula recomputes on every load regardless of what happens to be sitting in dataSourceUnchanged.

A cell with NO comment doesn't need preserving either way: it never differed from raw (dataSourceUnchanged already matches computed), or the user already reverted it (dataSourceUnchanged's raw value already matches the reverted plain value sitting in dataSource) - in both cases the baseline is already correct.

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

getFormulaColumnNames
Type : unknown
Default value : (dqRules: DQRule[]): string[] => dqRules .filter( (rule) => rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA' ) .map((rule) => rule.BASE_COL)

src/app/shared/utils/get-malformed-adapter-response-message.ts

getMalformedAdapterResponseMessage
Type : unknown
Default value : ( adapterResponse: unknown ): string | null => { if (adapterResponse !== null && typeof adapterResponse === 'object') { return null } return `The startupservice response was not in the expected format - it should be a JSON object, but the following was received instead:\n\n${String(adapterResponse)}` }

A successful startupservice/deploy-validation response is always a plain JSON object. When SAS Viya's Compute service can't run the underlying job (e.g. computeTasks misconfigured), it can return a 202 whose body is a plain-text "Job error ... path: /compute/tasks ... correlator: ..." block instead -

src/app/services/startup-check.service.ts

getSasjsTagDetails
Type : unknown
Default value : (element: Element): StartupStepDetail[] => Array.from(element.attributes).map((attribute) => ({ name: TAG_ATTRIBUTE_NAMES[attribute.name.toLowerCase()] || attribute.name, value: attribute.value || '(empty)' }))

Reads the attributes of the <sasjs> tag for display in the startup checks. Every attribute is shown - including ones the parser does not know about, so a misspelt one is visible and a wrong value is exactly the misconfiguration this dump exists to expose.

SASJS_TAG_STEP
Type : number
Default value : 3

Index of the <sasjs> tag dump within the startup steps. It is appended after the three startup checks so the indices the startup flow already drives (sas.service / app.service) are unchanged.

TAG_ATTRIBUTE_NAMES
Type : Record<string, string>
Default value : { serverurl: 'serverUrl', apploc: 'appLoc', servertype: 'serverType', loginmechanism: 'loginMechanism', admingroup: 'adminGroup', dcpath: 'dcPath', debug: 'debug', usecomputeapi: 'useComputeApi', runastask: 'runAsTask', contextname: 'contextName', hotlicencekey: 'hotLicenceKey' }

Canonical spelling of the <sasjs> tag attributes. HTML lower-cases attribute names in the DOM, so they are mapped back for display.

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

getStableFormulaBaseCols
Type : unknown
Default value : (dqRules: DQRule[]): string[] => dqRules .filter( (rule) => (rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA') && !/DC\.(USER_NAME|ORIG_VALUE|ROW_STATUS)/.test(rule.RULE_VALUE) ) .map((rule) => rule.BASE_COL)

BASE_COL names of every HARDFORMULA/SOFTFORMULA rule whose result is expected to be stable for a given row - i.e. excludes rules referencing DC.USER_NAME/DC.ORIG_VALUE/DC.ROW_STATUS. Those three are inherently session/edit-state-dependent (who's editing, what the row looked like before, whether it's currently modified) - comparing their live result against a frozen raw value doesn't mean "the formula overwrote real data" the way it does for a plain column-arithmetic formula (e.g. A_COL * B_COL); it would just always differ, for reasons unrelated to the dataset ever having real data there.

src/app/_globals.ts

globals
Type : { embed: boolean | "va"; vaApplyMode: "live" | "confirm"; rootParam: string; dcLib: string; xlmaps: XLMapListItem[]; editor: any; viewer: any; viewboxes: ViewboxCache; lineage: any; metadata: any; viyaApi: any; usernav: any; operators: any; handsontable: HandsontableStaticConfig }
Default value : { embed: false, vaApplyMode: 'live', rootParam: <string>'', dcLib: '', xlmaps: [], editor: { startupSet: <boolean>false, treeNodeLibraries: <any[] | null>[], libsAndTables: <any[]>[], libraries: <string[] | undefined>[], library: <string>'', table: <string>'', filter: <FilterCache>{ cols: <any[]>[], vals: <any[]>[], groupLogic: <string>'', whereClause: <string>'', libds: <string>'', clauses: <any>[], query: <QueryClause[]>[] } }, viewer: { startupSet: <boolean>false, tablesSet: <boolean>false, libraries: <any[]>[], tables: <any>null, library: '', table: '', libinfo: [], librariesSearch: <string>'', filter: <FilterCache>{ cols: <any[]>[], vals: <any[]>[], groupLogic: <string>'', whereClause: <string>'', libds: <string>'', clauses: <any>[], query: <QueryClause[]>[] }, currentSelection: <string>'' }, viewboxes: {}, lineage: { libraryList: <any[] | undefined>[], tablesList: <any[] | undefined>[], columnsList: <any[] | undefined>[], librariesSearch: <string>'', lib: <String | null>'', table: <String | undefined>'', column: <String | undefined>'', currentLineagePathLibTable: <String>'', currentLineagePathColumn: <String>'' }, metadata: { metaDataList: <any[] | undefined>undefined, metaDataSearch: <string>'', metaObjectList: <any[] | undefined>[], metaObjectSearch: <string>'', metaRepositories: <any[] | undefined>undefined, selectedRepository: <string>'' }, viyaApi: { collectionsList: <any[] | undefined>undefined, collectionsSearch: <string>'', selectedRepository: <string>'' }, usernav: { userList: <any[] | undefined>undefined, userSearch: <string>'', groupList: <any[] | undefined>undefined, groupSearch: <string>'', roleList: <any[] | undefined>undefined, roleSearch: <string>'' }, operators: { numOperators: ['=', '<', '>', '<=', '>=', 'BETWEEN', 'IN', 'NOT IN', 'NE'], charOperators: ['=', '<', '>', '<=', '>=', 'CONTAINS', 'IN', 'NOT IN', 'NE'] }, handsontable: { darkTableHeaderClass: 'darkTH' }, userDropdownConfig: { closeOnDebugClick: false } }

Cached filtering values across whole app (editor, viewer, viewboxes) Cached lineage libraries, tables Cached metadata tree Cached usernav tree Cached viyaApi collections, search and selected endpoint

initFilter
Type : { filter: FilterCache }
Default value : { filter: { cols: <any[]>[], vals: <any[]>[], groupLogic: <string>'', whereClause: <string>'', libds: <string>'', clauses: <any>[], query: <QueryClause[]>[] } }

Initial values when no cached values stored

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

hasFormulaRules
Type : unknown
Default value : (dqRules: DQRule[]): boolean => dqRules.some( (rule) => rule.RULE_TYPE === 'HARDFORMULA' || rule.RULE_TYPE === 'SOFTFORMULA' )

Whether any rule in the set is HARDFORMULA/SOFTFORMULA - used to gate turning on Handsontable's formulas plugin (HyperFormula engine) only when a table actually uses it, rather than for every grid.

src/crypto-shim.ts

HASHES
Type : []
Default value : [ 'md4', 'md5', 'ripemd160', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512' ]

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

INTL_TYPE_MAP
Type : Record<string, string>
Default value : { date: 'intl-date', time: 'intl-time', datetime: 'intl-datetime' }

Maps SAS' semantic date/time/datetime column types to the HOT 17 Intl cell type names. SAS sends a HOT-agnostic flag (date/time/datetime); the frontend owns the HOT-specific cell-type names:

  • date → intl-date (native date picker)
  • time → intl-time (native time picker)
  • datetime → intl-datetime (native datetime-local picker)

The cell display is the raw stored ISO string (locale-independent) — see registerIntlCellTypes, where these types' valueFormatter is the identity passthrough. No locale/format meta is attached; it is unused for display, and the validators are ISO-based and locale-agnostic. Runs in the DcValidator constructor, before the rules reach Handsontable.

mapIntlCellTypes
Type : unknown
Default value : (rules: DcValidation[]): DcValidation[] => { for (const rule of rules) { const mapped = rule.type ? INTL_TYPE_MAP[rule.type as string] : undefined if (mapped) rule.type = mapped } return rules }

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

isCharacterColumn
Type : unknown
Default value : (cols: Col[], colName: string): boolean => cols.find((col) => col.NAME === colName)?.DDTYPE === 'C'

DDTYPE is the backend's own char/numeric classification per column ('C' or 'N') - distinct from Col.TYPE, which mergeColsRules derives separately from $sasdata.vars (values 'char'/'num', not 'C'/'N').

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

isRegexRuleExempt
Type : unknown
Default value : ( value: any, isNumeric: boolean = false ): boolean => { if (value === undefined || value === null || value === '') return true return isNumeric && value === '.' }

HARDREGEX/SOFTREGEX both skip pattern-matching for:

  • blank values (undefined, null, '') on any column type - enforcing populated values is NOTNULL's job, not the pattern's; and
  • the plain SAS numeric missing (".") on numeric columns - it represents the absence of a value, same as blank.

SPECIAL missings (.a-.z, ._, bare letters) are NOT exempt, even on numeric columns: being deliberately set, they are real values the pattern is meant to check. This also means isSpecialMissing from

src/app/editor/utils/types.utils.ts

isStringDecimal
Type : unknown
Default value : (str: string) => { if (/^\d+\.\d+$/.test(str)) { return true } else { return false } }
isStringNumber
Type : unknown
Default value : (str: string) => { if (/^-{0,1}\d+$/.test(str)) { return true } else { return false } }

src/app/services/helper.service.ts

librariesToShow
Type : number
Default value : 50
sasMonths
Type : []
Default value : [ 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' ]

Month names used by a SAS date literal, eg 20APR2042.

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

mergeColsRules
Type : unknown
Default value : ( cols: Col[], rules: DcValidation[], $dataFormats: $DataFormats ): DcValidation[] => { for (let col of cols) { const rule = rules.find((x: DcValidation) => x.data === col.NAME) const colFormats = $dataFormats.vars[col.NAME] // Update COLS fields - it's passed by reference so this will update main object in editor component // DATE and TIME types are always numeric if (colFormats) col.TYPE = ['DATE', 'DATETIME', 'TIME'].includes(col.DDTYPE) ? 'num' : colFormats.type if (rule && col.DESC) rule.desc = col.DESC if (rule && colFormats?.length) rule.length = parseInt(colFormats.length) if (rule && col.CLS_RULE) rule.clsRule = col.CLS_RULE if (rule && colFormats?.type) rule.sasType = colFormats.type } return rules }

Merging old validation params from sasparams with cols params

src/app/lineage/lineage.component.ts

moment
Type : unknown
Default value : require('moment')

src/app/editor/utils/normalizeSortConfig.ts

normalizeSortConfig
Type : unknown
Default value : (cfg: any): any[] => Array.isArray(cfg) ? cfg : cfg ? [cfg] : []

multiColumnSorting's getSortConfig() returns a single config, an array of them, or undefined depending on how many columns are currently sorted - normalizes all three shapes into one array so callers never need three branches to handle it. SortConfig isn't part of Handsontable's public API surface in this version (only re-exported internally), and its own .d.ts shape isn't structurally assignable to Record<string, unknown> either (a specific interface isn't assignable to an index-signature type without one of its own), so any is the pragmatic choice here rather than fighting the type system for a type this file can't even name.

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

parseColTypeRow
Type : unknown
Default value : ( coltype: string | undefined ): HotColumnSettings | undefined => { if (!coltype) return undefined try { return JSON.parse(coltype) } catch (err: any) { return undefined } }

Parses a single column's COLTYPE JSON-object string (one per cols row, as of the vertical-array getdata.sas fix — see issue #253). Unlike the previous comma-joined sasparams.COLTYPE format, each row is independent: a malformed or missing value here only drops that one column's rule, rather than every column's.

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

prepareDeleteRecordColumn
Type : unknown
Default value : (response: DeleteColumnResponse) => { const sasparams = response.data.sasparams[0] const sentByBackend = sasparams.COLHEADERS.split(',').includes(DELETE_RECORD_COLUMN_NAME) || (response.data.cols || []).some( (col) => col.NAME === DELETE_RECORD_COLUMN_NAME ) || Object.prototype.hasOwnProperty.call( response.data.$sasdata?.vars || {}, DELETE_RECORD_COLUMN_NAME ) if (sentByBackend) { throw new Error( `getdata returned the reserved column ${DELETE_RECORD_COLUMN_NAME}. ` + 'That name is created by the editor itself and must not exist in the target table.' ) } sasparams.COLHEADERS = `${DELETE_RECORD_COLUMN_NAME},${sasparams.COLHEADERS}` }

Asserts that getdata did NOT send the client-owned delete column, then prepends it to COLHEADERS so the grid's first column is the delete flag.

_____DELETE__THIS__RECORD_____ is created by the editor itself (see deleteRecordColumnRule.ts) and getdata must never emit it. Its presence in a response can only mean a real table column of that name, or a hook that re-added it - either way it would shadow the delete flag, so refuse the load rather than render a table whose delete column cannot be trusted. Callers run this inside a promise chain whose .catch surfaces the error (the editor sets getdataError).

Call it BEFORE constructing a DcValidator or deriving headerArray: rules, columnHeader and headerArray are all keyed off COLHEADERS order, and the delete column is always index 0.

seedDeleteRecordColumn
Type : unknown
Default value : (rows: any[]) => { for (const row of rows) { row[DELETE_RECORD_COLUMN_NAME] = 'No' } }

Seeds every row's delete flag to 'No' - getdata no longer sends the column, so the editor owns the value too (see deleteRecordColumnRule.ts).

src/app/shared/hot-export/hot-export.util.ts

RAW_EXPORT_TYPES
Type : unknown
Default value : new Set([ 'date', 'time', 'datetime', 'intl-date', 'intl-time', 'intl-datetime' ])

Cell types whose values must be exported as raw stored strings rather than converted to native Excel date/time cells. SAS raw data is the canonical representation, so dates/times are exported exactly as the grid stores them. numeric is intentionally NOT listed — it stays a native Excel number.

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

resolveRevertedCellValue
Type : unknown
Default value : ( rawValueText: string, isNumericCol: boolean ): string | number => { if (!isNumericCol) return rawValueText const numericValue = Number(rawValueText) return Number.isNaN(numericValue) ? rawValueText : numericValue }

src/app/app.routing.ts

ROUTES
Type : Routes
Default value : [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'view', loadChildren: () => ViewerModule }, { /** * Load review module (approve, history, submitted) */ path: 'review', component: ReviewRouteComponent, children: [ { path: '', pathMatch: 'full', redirectTo: 'toapprove' }, { path: '', loadChildren: () => ReviewModule } ] }, { path: 'licensing', loadChildren: () => LicensingModule }, { path: 'home', loadChildren: () => HomeModule }, { /** * Load editor module with subroutes */ path: 'editor', loadChildren: () => EditorModule }, { path: 'stage', loadChildren: () => StageModule }, { path: 'system', loadChildren: () => SystemModule }, { path: 'deploy', loadChildren: () => DeployModule }, { path: '**', component: NotFoundComponent } ]

Defining routes

ROUTING
Type : ModuleWithProviders<RouterModule>
Default value : RouterModule.forRoot( ROUTES, { useHash: true } )

Exporting routes

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

schemaTypeMap
Type : { }
Default value : { numeric: '', default: '' }

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

substituteColumnReferences
Type : unknown
Default value : ( formulaText: string, columnNames: string[], rowIndex: number ): string => { const hasLeadingEquals = formulaText.startsWith('=') const formulaBody = hasLeadingEquals ? formulaText.slice(1) : formulaText const quotedSpanPattern = /"[^"]*"|'[^']*'/g let result = '' let lastIndex = 0 let match: RegExpExecArray | null const substituteUnquotedSpan = (span: string): string => { let substituted = span columnNames.forEach((columnName, columnIndex) => { const cellRef = `${Handsontable.helper.spreadsheetColumnLabel(columnIndex)}${rowIndex + 1}` substituted = substituteBoundedToken(substituted, columnName, cellRef) }) return substituted } while ((match = quotedSpanPattern.exec(formulaBody))) { result += substituteUnquotedSpan(formulaBody.slice(lastIndex, match.index)) result += match[0] lastIndex = match.index + match[0].length } result += substituteUnquotedSpan(formulaBody.slice(lastIndex)) return hasLeadingEquals ? `=${result}` : result }

Translates column names in a formula-like string into this row's cell references, e.g. "=PRICE * VOLUME" -> "=B4 * C4" for row index 3. Column-name substitution only - no DC.* variable support (that's specific to admin-defined rule values re-evaluated per row - see parseFormulaRule.ts, which this deliberately does not delegate to, to keep this scoped to exactly column names).

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

syncOverwrittenCellComment
Type : unknown
Default value : ( currentValue: unknown, rawValue: unknown, hasCommentAlready: boolean ): OverwrittenCommentAction => { const hasMeaningfulRawValue = rawValue !== undefined && rawValue !== null && rawValue !== '' const isOverwritten = hasMeaningfulRawValue && String(rawValue) !== String(currentValue) if (isOverwritten) return hasCommentAlready ? 'none' : 'set' return hasCommentAlready ? 'remove' : 'none' }

Decides what a single cell's "overwritten" comment should do in response to a live edit (afterChange) - detection must run on every edit, since an edit can just as easily make an already-overwritten cell match its raw value again (typed back by hand) as it can make an untouched cell diverge from it. Uses the same "loosely equal, blank raw is never meaningful" rule as findOverwrittenCells, since a single afterChange call operates on one cell at a time, not the whole-grid batch that function expects.

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

unescapeFormula
Type : unknown
Default value : (value: unknown): unknown => { if (typeof value !== 'string') return value if (value.charAt(0) !== "'" || value.charAt(1) !== '=') return value return value.slice(1) }

Inverse of escapeCharacterColumnValue - strips the leading ' this app itself added to keep a backend =-led value out of HyperFormula's evaluation. Only ever called for a cell autoEscapedCellTracker confirms the app itself marked - see character-column-formula-plan.md.

src/environments/version.ts

VERSION
Type : object
Default value : { "dirty": false, "raw": "dd482c4", "hash": "dd482c4", "distance": null, "tag": null, "semver": null, "suffix": "dd482c4", "semverString": null, "timestamp": "1793153169099", "adapterVersion": "5.1.2" }

src/app/editor/utils/withoutEditStatus.ts

withoutEditStatus
Type : unknown
Default value : (row: any): any => { const { [EDIT_STATUS_COLUMN_NAME]: _editStatus, ...rest } = row return rest }

Returns a copy of row without the client-only EDIT_STATUS column (see editStatusColumnRule.ts) - shared by classifyRow (which must ignore it when diffing, or writing its own prior output back would self-perpetuate) and by anywhere a row is sent to the backend (which must never receive it at all).

results matching ""

    No results matching ""