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.
296 lines
12 KiB
JavaScript
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')
|