fix(mocks): key rk_underlying tables on their business key
getdata.sas exposes coalesce(rk_underlying, buskey) as the PK and drops the RK column, so the editor shows the business key as read-only and never sends the RK. The JS mock still published the RK as the PK and left it in the grid, and stagedata had no loader to derive it back, so an edit to such a table (MPE_ROW_LEVEL_SECURITY, MPE_SELECTBOX) could not be diffed. The mock now mirrors that contract: PK/PKCNT come from rk_underlying, the RK column is withheld and the PK variables lead COLHEADERS, and stagedata assigns the RK from the business key - keeping the one the base row already holds and minting the next value for a new key.
This commit is contained in:
1 parent
779006b0bf
commit
347dbb53b0
2 files changed
+78
-7
No files matched your search
@@ -77,6 +77,27 @@ if (mpeTablesData && mpeTablesData.rows) {
|
||||
tableReg = mpeTablesData.rows.find(r => r.libref === requestedLibref && r.dsn === requestedTable)
|
||||
}
|
||||
|
||||
// The variables exposed to the editor as the primary key, matching getdata.sas:
|
||||
// pk = coalesce(rk_underlying, buskey). A table registered with rk_underlying
|
||||
// is keyed on its business key in the editor - the RK itself is assigned by the
|
||||
// backend loader, so it is neither shown nor sent by the user.
|
||||
const rkUnderlying = tableReg && tableReg.rk_underlying ? tableReg.rk_underlying.trim() : ''
|
||||
const buskey = tableReg && tableReg.buskey ? tableReg.buskey.trim() : ''
|
||||
const pkFields = rkUnderlying || buskey
|
||||
const pkColumns = pkFields ? pkFields.split(/\s+/) : []
|
||||
|
||||
// Order columns with the PK variables first, in PK order, then the remaining
|
||||
// columns in schema order. getdata.sas does the same ("order delete var and pk
|
||||
// fields at start of table") and the client reads the first PKCNT columns
|
||||
// positionally when it detects duplicate keys.
|
||||
function pkFirst(columns, keys) {
|
||||
const rank = new Map(keys.map((name, i) => [name.toUpperCase(), i]))
|
||||
const keyColumns = columns
|
||||
.filter(col => rank.has(col.name.toUpperCase()))
|
||||
.sort((a, b) => rank.get(a.name.toUpperCase()) - rank.get(b.name.toUpperCase()))
|
||||
return keyColumns.concat(columns.filter(col => !rank.has(col.name.toUpperCase())))
|
||||
}
|
||||
|
||||
// ─── DQ rules (from MPE_VALIDATIONS + NOTNULL from schema) ──────────────────
|
||||
// Mirrors getdata.sas: dqrules = MPE_VALIDATIONS rows UNION dictionary.columns
|
||||
// NOTNULL constraints. MPE_VALIDATIONS lives in the control library.
|
||||
@@ -85,10 +106,7 @@ const validationsData = mpeLoadTableData('MPE_VALIDATIONS')
|
||||
let dqrules = []
|
||||
let dqdata = []
|
||||
if (validationsData && validationsData.rows) {
|
||||
const pkCols = new Set()
|
||||
if (tableReg && tableReg.buskey) {
|
||||
tableReg.buskey.trim().split(/\s+/).forEach(c => pkCols.add(c.toUpperCase()))
|
||||
}
|
||||
const pkCols = new Set(pkColumns.map(c => c.toUpperCase()))
|
||||
dqrules = validationsData.rows
|
||||
.filter(r => r.base_lib === requestedLibref && r.base_ds === requestedTable)
|
||||
.map(r => ({
|
||||
@@ -206,8 +224,17 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
if (tableReg.var_busfrom) excludeCols.add(tableReg.var_busfrom.toUpperCase())
|
||||
if (tableReg.var_busto) excludeCols.add(tableReg.var_busto.toUpperCase())
|
||||
}
|
||||
// getdata.sas drops the RK field from the editor view when the table is
|
||||
// registered with rk_underlying (drop_rk=&buskey), and exposes the business
|
||||
// key instead.
|
||||
if (rkUnderlying && buskey) {
|
||||
buskey.split(/\s+/).forEach(c => excludeCols.add(c.toUpperCase()))
|
||||
}
|
||||
|
||||
let visibleColumns = tableData.columns.filter(c => !excludeCols.has(c.name.toUpperCase()))
|
||||
let visibleColumns = pkFirst(
|
||||
tableData.columns.filter(c => !excludeCols.has(c.name.toUpperCase())),
|
||||
pkColumns
|
||||
)
|
||||
let visibleRows = tableData.rows
|
||||
.filter(r => filterPredicate(r))
|
||||
.map(r => {
|
||||
@@ -363,8 +390,8 @@ if (tableData && tableData.columns && tableData.rows) {
|
||||
sasparams: [{
|
||||
COLHEADERS: colHeaders.join(','),
|
||||
FILTER_TEXT: filterText === '' ? ' ' : filterText,
|
||||
PKCNT: tableReg && tableReg.buskey ? tableReg.buskey.trim().split(/\s+/).length : 0,
|
||||
PK: tableReg ? tableReg.buskey || '' : '',
|
||||
PKCNT: pkColumns.length,
|
||||
PK: pkFields,
|
||||
DTVARS: dtVars.length ? ' ' + dtVars.join(' ') : '',
|
||||
DTTMVARS: dttmVars.length ? ' ' + dttmVars.join(' ') : '',
|
||||
TMVARS: tmVars.length ? ' ' + tmVars.join(' ') : '',
|
||||
|
||||
@@ -81,6 +81,50 @@ if (hookReg && hookReg.post_edit_hook) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RK assignment (mpe_loader) ──────────────────────────────────────────────
|
||||
// A table registered with rk_underlying is edited on its business key and the
|
||||
// RK column is withheld from the editor (see getdata.js), so the submitted rows
|
||||
// carry no RK. The loader derives it: %mpe_loader hands RK_UNDERLYING to
|
||||
// %bitemporal_dataloader, which keeps the RK the business key already holds and
|
||||
// mints the next value for a new key.
|
||||
|
||||
const valueOf = (row, name) => {
|
||||
if (row[name] !== undefined) return row[name]
|
||||
const key = Object.keys(row).find((k) => k.toUpperCase() === name.toUpperCase())
|
||||
return key === undefined ? undefined : row[key]
|
||||
}
|
||||
|
||||
const finalTables = makeTableLoader(mpeDataDir)('MPE_TABLES')
|
||||
const finalReg =
|
||||
finalTables && finalTables.rows
|
||||
? finalTables.rows.find((r) => r.libref === libref && r.dsn === dsn)
|
||||
: null
|
||||
const rkCols =
|
||||
finalReg && finalReg.rk_underlying ? finalReg.rk_underlying.trim().split(/\s+/) : []
|
||||
const rkCol = finalReg && finalReg.buskey ? finalReg.buskey.trim() : ''
|
||||
const sendsRk =
|
||||
rkCol !== '' && jsDataHeaders.some((h) => h.toUpperCase() === rkCol.toUpperCase())
|
||||
|
||||
if (rkCols.length > 0 && rkCol !== '' && !sendsRk) {
|
||||
const baseData = loadTableData(dsn) || { columns: [], rows: [] }
|
||||
const rkName =
|
||||
(baseData.columns.find((c) => c.name.toUpperCase() === rkCol.toUpperCase()) || {}).name ||
|
||||
rkCol
|
||||
const businessKey = (row) =>
|
||||
rkCols.map((c) => String(valueOf(row, c) ?? '').trim().toUpperCase()).join('|')
|
||||
const baseByKey = new Map()
|
||||
let nextRk = 0
|
||||
for (const row of baseData.rows) {
|
||||
baseByKey.set(businessKey(row), valueOf(row, rkName))
|
||||
const rk = Number(valueOf(row, rkName))
|
||||
if (isFinite(rk) && rk > nextRk) nextRk = rk
|
||||
}
|
||||
for (const row of jsDataRows) {
|
||||
const existing = baseByKey.get(businessKey(row))
|
||||
row[rkName] = existing === undefined || existing === null ? ++nextRk : existing
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stage the data ──────────────────────────────────────────────────────────
|
||||
// One subfolder per submission (DSID) under the staging directory, so that the
|
||||
// staged data can sit alongside related artifacts (submit/approval logs,
|
||||
|
||||
Reference in new issue
Block a user