Files
dc/sas/mocks/sasjs/services/editors/getdynamiccolvals.js
T
dc ce6bb5d4cd
Build / Build-and-ng-test (pull_request) Successful in 5m44s
Lighthouse Checks / lighthouse (pull_request) Successful in 10m9s
Build / Build-and-test-development (pull_request) Canceled after 0s
feat(mocks): build every JS stored program's response with webout()
The last eleven services still built the response envelope through
dcMockUtils' webOutOpen/webOutObj/webOutClose. They now use the webout() the
SASjs Server emits into every JS program, so dcMockUtils no longer emulates
%webout at all - the helper existed only for these callers and is deleted
with them.

That needed the options object added to webout() in @sasjs/utils 4.6
(showmeta, maxobs), which is what a clean install of @sasjs/server@3.12.2
resolves to (^4.4.0 -> 4.6.1, checked). A server whose install still holds
4.4/4.5 refuses the option by name rather than ignoring it, so the failure is
loud rather than a response that is quietly missing its column metadata.

getdata, viewdata and getcolvals send their metadata as
`webout('OBJ', name, rows, { showmeta })`, and every other call keeps its
member name and rows unchanged.

One call was positional all along: startupservice's xlmaps is an array of
arrays, which the old helper serialised as-is and webout() refuses for OBJ.
It is now webout('ARR', 'xlmaps', xlmaps) - caught by the comparison run
below, not by the rewrite.
2026-11-03 23:16:48 +00:00

296 lines
12 KiB
JavaScript

const nodePath = require('path')
let appLoc = nodePath.join(..._program.split('services')[0].split('/'))
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
const dcLibref = 'DC_JSLIB'
// Load shared DC mock utilities
eval(
fs.readFileSync(
nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'),
'utf8'
)
)
// MPE_VALIDATIONS / MPE_TABLES / MPE_GROUPS / MPE_SECURITY live in the
// control library (DC_JSLIB / mpelib).
const mpeDataDir = libDataDir(dcLibref)
const mpeLoadTableData = makeTableLoader(mpeDataDir)
// ─── Parse input ──────────────────────────────────────────────────────────────
// Mirrors getdynamiccolvals.sas: SASControlTable has LIBDS and VARIABLE_NM,
// and a source_row table with the current row's values.
const _sctRow = fetchTable('SASControlTable')[0] || {}
const libds = (_sctRow.LIBDS || _sctRow.libds || '').toString().toUpperCase()
const variableNm = (_sctRow.VARIABLE_NM || _sctRow.variable_nm || '').toString()
const sourceRow = fetchTable('source_row')[0] || {}
const libref = libds.split('.')[0]
const dsn = libds.split('.')[1] || ''
// ─── Look up the hook program in MPE_VALIDATIONS ──────────────────────────────
// Mirrors the codetest data step in getdynamiccolvals.sas: find the
// SOFTSELECT_HOOK / HARDSELECT_HOOK rule for this libds.column.
const validationsData = mpeLoadTableData('MPE_VALIDATIONS')
let hookProgram = null
if (validationsData && validationsData.rows) {
const rule = validationsData.rows.find(
(r) =>
r.base_lib === libref &&
r.base_ds === dsn &&
r.base_col === variableNm &&
(r.rule_type === 'SOFTSELECT_HOOK' ||
r.rule_type === 'HARDSELECT_HOOK') &&
r.rule_active === 1 &&
(r.rule_value || '').length > 1
)
if (rule) hookProgram = rule.rule_value
}
// ─── Execute the hook program (emulate each services/validations/*.sas) ──────
// Each hook reads the actual seeded data, the same way the SAS hook reads the
// real table via proc sql. source_row values are used where the hook needs the
// current row's LIBREF/DSN to resolve a target table.
let dynamicValues = []
let dynamicExtendedValues = []
function upcase(v) {
return v === null || v === undefined ? '' : String(v).toUpperCase()
}
// Helper: find a value in source_row case-insensitively
function srcVal(name) {
if (!sourceRow) return undefined
const lc = name.toLowerCase()
if (sourceRow[name] !== undefined) return sourceRow[name]
if (sourceRow[lc] !== undefined) return sourceRow[lc]
const key = Object.keys(sourceRow).find((k) => k.toLowerCase() === lc)
return key ? sourceRow[key] : undefined
}
if (hookProgram) {
// Normalise: strip any appLoc prefix the rule_value may carry
const hook = hookProgram.replace(
/^.*\/services\/validations\//,
'services/validations/'
)
console.log('[DEBUG] hookProgram:', hookProgram)
console.log('[DEBUG] hook (normalised):', hook)
// Mirror getdynamiccolvals.sas: %mp_include(sascode) executes the hook
// program in the SAME scope, so work.dynamic_values and
// work.dynamic_extended_values are available to it. We do the same:
// eval the hook .js file in this scope so it can push to
// dynamicValues / dynamicExtendedValues directly. Falls back to the
// inline emulations below when no .js file exists for a hook yet.
//
// hook is relative to appLoc (e.g. 'services/validations/mpe_tables.dsn'),
// the same way dcMockUtils.js is loaded above and the same way the SAS
// %include resolves rule_value against &appLoc.
const hookFile = nodePath.resolve(driveRoot, 'files', appLoc, hook + '.js')
if (fs.existsSync(hookFile)) {
eval(fs.readFileSync(hookFile, 'utf8'))
} else if (hook === 'services/validations/columns_in_libds') {
// columns_in_libds.sas: resolves the target libds from source_row
// depending on which table is being edited (MPE_EXCEL_CONFIG,
// MPE_VALIDATIONS, MPE_SELECTBOX, MPE_ROW_LEVEL_SECURITY,
// MPE_COLUMN_LEVEL_SECURITY, MPE_TABLES), then proc contents on it.
let tgtlibds = ''
const sLibds = libds // the table being edited (DC_JSLIB.MPE_*)
if (sLibds === dcLibref + '.MPE_EXCEL_CONFIG')
tgtlibds = upcase(srcVal('XL_LIBREF')) + '.' + upcase(srcVal('XL_TABLE'))
else if (sLibds === dcLibref + '.MPE_VALIDATIONS')
tgtlibds = upcase(srcVal('BASE_LIB')) + '.' + upcase(srcVal('BASE_DS'))
else if (sLibds === dcLibref + '.MPE_SELECTBOX')
tgtlibds =
upcase(srcVal('SELECT_LIB')) + '.' + upcase(srcVal('SELECT_DS'))
else if (sLibds === dcLibref + '.MPE_ROW_LEVEL_SECURITY')
tgtlibds =
upcase(srcVal('RLS_LIBREF')) + '.' + upcase(srcVal('RLS_TABLE'))
else if (sLibds === dcLibref + '.MPE_COLUMN_LEVEL_SECURITY')
tgtlibds =
upcase(srcVal('CLS_LIBREF')) + '.' + upcase(srcVal('CLS_TABLE'))
else if (sLibds === dcLibref + '.MPE_TABLES')
tgtlibds = upcase(srcVal('LIBREF')) + '.' + upcase(srcVal('DSN'))
if (tgtlibds && tgtlibds.includes('.')) {
const [tLib, tDs] = tgtlibds.split('.')
const found = loadTableAnyLib(tDs)
if (found && found.data && found.data.columns) {
for (const col of found.data.columns) {
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: String(col.name).toUpperCase()
})
}
}
}
} else if (hook === 'services/validations/sas_groups') {
// sas_groups.sas: distinct groupname from work.groups (mpe_getgroups)
const groupsData = mpeLoadTableData('MPE_GROUPS')
if (groupsData && groupsData.rows) {
const seen = new Set()
for (const r of groupsData.rows) {
const g = r.group_name
if (g && !seen.has(g)) {
seen.add(g)
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: g
})
}
}
dynamicValues.sort((a, b) =>
String(a.RAW_VALUE).localeCompare(String(b.RAW_VALUE))
)
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
}
} else if (hook === 'services/validations/libraries_all') {
// libraries_all.sas: distinct upcase(libraryref) from mm_getLibs
for (const lib of allLibrefs()) {
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: lib.toUpperCase()
})
}
dynamicValues.sort((a, b) =>
String(a.RAW_VALUE).localeCompare(String(b.RAW_VALUE))
)
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
} else if (hook === 'services/validations/libraries_editable') {
// libraries_editable.sas: distinct upcase(libref) from mpe_tables
const mpeTablesData = mpeLoadTableData('MPE_TABLES')
if (mpeTablesData && mpeTablesData.rows) {
const seen = new Set()
for (const r of mpeTablesData.rows) {
if (r.libref && !seen.has(r.libref.toUpperCase())) {
seen.add(r.libref.toUpperCase())
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: r.libref.toUpperCase()
})
}
}
dynamicValues.sort((a, b) =>
String(a.RAW_VALUE).localeCompare(String(b.RAW_VALUE))
)
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
}
} else if (hook === 'services/validations/tables_all') {
// tables_all.sas: resolves tgtlib from source_row, lists all member
// names (tables) in that library from dictionary.members
const tgtlib = upcase(
srcVal('LIBREF') ||
srcVal('XL_LIBREF') ||
srcVal('BASE_LIB') ||
srcVal('SELECT_LIB') ||
srcVal('RLS_LIBREF') ||
srcVal('CLS_LIBREF')
)
if (tgtlib) {
const dir = libDataDir(tgtlib)
try {
const files = fs.readdirSync(dir)
const seen = new Set()
for (const f of files) {
if (!f.endsWith('.json')) continue
const td = JSON.parse(
fs
.readFileSync(nodePath.resolve(dir, f), { encoding: 'utf8' })
.toString()
)
if (td.name && !seen.has(td.name)) {
seen.add(td.name)
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: td.name
})
}
}
dynamicValues.sort((a, b) =>
String(a.RAW_VALUE).localeCompare(String(b.RAW_VALUE))
)
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
} catch (err) {}
}
} else if (hook === 'services/validations/tables_editable') {
// tables_editable.sas: distinct upcase(dsn) from mpe_tables where
// libref = source_row's libref column (XL_LIBREF for MPE_EXCEL_CONFIG,
// libref for MPE_SECURITY / MPE_TABLES)
const srccol =
libds === dcLibref + '.MPE_EXCEL_CONFIG' ? 'XL_LIBREF' : 'LIBREF'
const tgtlib = upcase(srcVal(srccol))
const mpeTablesData = mpeLoadTableData('MPE_TABLES')
if (mpeTablesData && mpeTablesData.rows) {
const seen = new Set()
for (const r of mpeTablesData.rows) {
if (
upcase(r.libref) === tgtlib &&
r.dsn &&
!seen.has(r.dsn.toUpperCase())
) {
seen.add(r.dsn.toUpperCase())
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: r.dsn.toUpperCase()
})
}
}
dynamicValues.sort((a, b) =>
String(a.RAW_VALUE).localeCompare(String(b.RAW_VALUE))
)
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
}
} else if (hook === 'services/validations/mpe_x_test.some_num') {
// mpe_x_test.some_num.sas: select distinct some_num from &libds
// &libds resolves to the source table being edited (TESTDATA.MPE_X_NEW
// or DC_JSLIB.MPE_X_TEST depending on which table the hook was fired
// from).
const found = loadTableAnyLib(dsn)
if (found && found.data && found.data.rows) {
const seen = new Set()
for (const r of found.data.rows) {
let v = r[variableNm]
if (v === undefined) v = r[variableNm.toLowerCase()]
if (v !== undefined && v !== null && !seen.has(v)) {
seen.add(v)
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: v
})
}
}
dynamicValues.sort((a, b) => Number(a.RAW_VALUE) - Number(b.RAW_VALUE))
dynamicValues.forEach((v, i) => (v.DISPLAY_INDEX = i + 1))
}
} else if (hook === 'services/validations/mpe_alerts.alert_lib') {
// mpe_alerts.alert_lib.sas: distinct alert_lib values from MPE_ALERTS
const alertsData = mpeLoadTableData('MPE_ALERTS')
if (alertsData && alertsData.rows) {
const seen = new Set()
for (const r of alertsData.rows) {
if (r.alert_lib && !seen.has(r.alert_lib)) {
seen.add(r.alert_lib)
dynamicValues.push({
DISPLAY_INDEX: dynamicValues.length + 1,
RAW_VALUE: r.alert_lib
})
}
}
}
}
}
// ─── Respond ─────────────────────────────────────────────────────────────────
// getdynamiccolvals.sas ensures DISPLAY_INDEX exists on both tables and sends
// them with fmt=N. dynamic_extended_values is optional; we emit it empty (the
// shape is still required for the adapter to populate the client model).
webout('OPEN')
webout('OBJ', 'dynamic_values', dynamicValues)
webout('OBJ', 'dynamic_extended_values', dynamicExtendedValues)
webout('CLOSE')