fix: history mock
This commit is contained in:
@@ -4,33 +4,97 @@ let appLoc = nodePath.join(..._program.split('services')[0].split('/'))
|
||||
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
|
||||
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
|
||||
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, "files", appLoc, "services", "dcMockUtils.js"), "utf8"))
|
||||
eval(
|
||||
fs.readFileSync(
|
||||
nodePath.resolve(
|
||||
driveRoot,
|
||||
'files',
|
||||
appLoc,
|
||||
'services',
|
||||
'dcMockUtils.js'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const dcLibref = 'DC_JSLIB'
|
||||
const dataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref)
|
||||
|
||||
// Load MPE_SUBMIT - return all items (history shows everything)
|
||||
let fromsas = []
|
||||
try {
|
||||
const file = nodePath.resolve(dataDir, 'mpe_submit.json')
|
||||
const raw = fs.readFileSync(file, {encoding:'utf8'}).toString()
|
||||
const submitData = JSON.parse(raw)
|
||||
fromsas = submitData.rows.map(r => ({
|
||||
TABLE_ID: r.TABLE_ID,
|
||||
REVIEW_STATUS_ID: r.SUBMIT_STATUS_CD,
|
||||
SUBMITTED_BY_NM: r.SUBMITTED_BY_NM,
|
||||
BASE_TABLE: r.BASE_LIB + '.' + r.BASE_DS,
|
||||
SUBMITTED_ON_DTTM: new Date(r.SUBMITTED_ON_DTTM * 1000).toISOString().replace('T',' ').replace(/\.\d+Z$/, ''),
|
||||
SUBMITTED_ON_DTTM2: r.SUBMITTED_ON_DTTM,
|
||||
SUBMITTED_REASON_TXT: r.SUBMITTED_REASON_TXT || '',
|
||||
NUM_OF_APPROVALS_REQUIRED: r.NUM_OF_APPROVALS_REQUIRED,
|
||||
NUM_OF_APPROVALS_REMAINING: r.NUM_OF_APPROVALS_REMAINING,
|
||||
LIBREF: r.BASE_LIB,
|
||||
DSN: r.BASE_DS,
|
||||
REVIEWED_BY_NM: r.REVIEWED_BY_NM || '',
|
||||
REVIEWED_ON_DTTM: r.REVIEWED_ON_DTTM ? new Date(r.REVIEWED_ON_DTTM * 1000).toISOString().replace('T',' ').replace(/\.\d+Z$/, '') : null
|
||||
}))
|
||||
} catch(err) {}
|
||||
const loadTableData = makeTableLoader(dataDir)
|
||||
|
||||
// ─── Parse input (BrowserParams: STARTROW / HIST) ───────────────────────────
|
||||
|
||||
const browserParams = fetchTable('BrowserParams')[0] || {}
|
||||
const startRow = browserParams.STARTROW
|
||||
? parseInt(String(browserParams.STARTROW), 10)
|
||||
: 1
|
||||
let hist = 40
|
||||
if (browserParams.HIST) {
|
||||
hist = parseInt(String(browserParams.HIST), 10)
|
||||
} else {
|
||||
// fall back to MPE_CONFIG HISTORY_ROWS
|
||||
try {
|
||||
const cfg = loadTableData('MPE_CONFIG')
|
||||
const row = cfg.rows.find(
|
||||
(r) => r.var_scope === 'DC_REVIEW' && r.var_name === 'HISTORY_ROWS'
|
||||
)
|
||||
if (row) hist = parseInt(String(row.var_value), 10)
|
||||
} catch (e) {
|
||||
/* keep default */
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Load MPE_SUBMIT history rows (SUBMIT_STATUS_CD != 'SUBMITTED') ────────
|
||||
|
||||
const submitData = loadTableData('MPE_SUBMIT') || { rows: [] }
|
||||
|
||||
// Filter to approved/rejected (not 'SUBMITTED') and sort by submitted desc
|
||||
const historyRows = submitData.rows.filter(
|
||||
(r) => r.SUBMIT_STATUS_CD !== 'SUBMITTED'
|
||||
)
|
||||
|
||||
historyRows.sort((a, b) => b.SUBMITTED_ON_DTTM - a.SUBMITTED_ON_DTTM)
|
||||
|
||||
// Slice by startRow / hist
|
||||
const slicedRows = historyRows.slice(startRow - 1, startRow - 1 + hist)
|
||||
|
||||
// ─── Format output for the history grid ───────────────────────────────────────
|
||||
|
||||
const fromsas = slicedRows.map((r) => {
|
||||
const submitted = new Date(r.SUBMITTED_ON_DTTM * 1000)
|
||||
const reviewed = r.REVIEWED_ON_DTTM
|
||||
? new Date(r.REVIEWED_ON_DTTM * 1000)
|
||||
: null
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const fmt = (d) =>
|
||||
[d.getFullYear(), pad(d.getMonth() + 1), pad(d.getDate())].join('-') +
|
||||
' ' +
|
||||
[pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds())].join(':')
|
||||
|
||||
return {
|
||||
TABLE_ID: r.TABLE_ID,
|
||||
BASE_TABLE: r.BASE_LIB + '.' + r.BASE_DS,
|
||||
SUBMITTED: fmt(submitted),
|
||||
SUBMITTED_REASON_TXT: r.SUBMITTED_REASON_TXT || '',
|
||||
SUBMITTER: r.SUBMITTED_BY_NM || '',
|
||||
REVIEWED: reviewed ? fmt(reviewed) : ' ',
|
||||
STATUS: r.SUBMIT_STATUS_CD,
|
||||
REVIEWED_ON_DTTM: r.REVIEWED_ON_DTTM,
|
||||
APPROVER: r.REVIEWED_BY_NM || '',
|
||||
NUM_OF_APPROVALS_REQUIRED: r.NUM_OF_APPROVALS_REQUIRED || 1
|
||||
}
|
||||
})
|
||||
|
||||
// ─── Pagination params ────────────────────────────────────────────────────────
|
||||
|
||||
const histparams = [
|
||||
{
|
||||
HIST: hist,
|
||||
STARTROW: startRow,
|
||||
NOBS: historyRows.length
|
||||
}
|
||||
]
|
||||
|
||||
webOutOpen()
|
||||
webOutObj(fromsas, 'fromsas')
|
||||
webOutObj(histparams, 'histparams')
|
||||
webOutClose()
|
||||
|
||||
@@ -7,7 +7,12 @@ const dcLibref = 'DC_JSLIB'
|
||||
const dataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref)
|
||||
const stagingDir = nodePath.resolve(dataDir, 'staging')
|
||||
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
||||
eval(
|
||||
fs.readFileSync(
|
||||
nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
const loadTableData = makeTableLoader(dataDir)
|
||||
|
||||
// ─── Parse input ──────────────────────────────────────────────────────────────
|
||||
@@ -19,33 +24,45 @@ let stagetable = []
|
||||
let fmt_stagetable = []
|
||||
|
||||
if (tableId) {
|
||||
// staged data lives in a subfolder per submission (DSID) under staging/
|
||||
const stageFolder = nodePath.resolve(stagingDir, tableId)
|
||||
const candidates = [
|
||||
nodePath.resolve(stageFolder, 'staged.json'),
|
||||
// legacy layout: staging/<table_id>.json
|
||||
nodePath.resolve(stagingDir, tableId + '.json')
|
||||
]
|
||||
const stageFile = candidates.find((f) => fs.existsSync(f))
|
||||
if (stageFile) {
|
||||
try {
|
||||
const stageFile = nodePath.resolve(stagingDir, tableId + '.json')
|
||||
const stageData = JSON.parse(fs.readFileSync(stageFile, {encoding:'utf8'}).toString())
|
||||
const stageData = JSON.parse(
|
||||
fs.readFileSync(stageFile, { encoding: 'utf8' }).toString()
|
||||
)
|
||||
|
||||
stagetable = stageData.rows
|
||||
stagetable = stageData.rows
|
||||
|
||||
// Build fmt_stagetable by applying SAS formats to numeric temporal columns
|
||||
const dsn = stageData.dsn || ''
|
||||
const schema = loadTableSchema(dataDir, dsn)
|
||||
const colDdtypes = {}
|
||||
for (const col of schema) colDdtypes[col.name.toUpperCase()] = getDdType(col)
|
||||
// Build fmt_stagetable by applying SAS formats to temporal columns
|
||||
const dsn = stageData.dsn || ''
|
||||
const schema = loadTableSchema(dataDir, dsn)
|
||||
const colDdtypes = {}
|
||||
for (const col of schema)
|
||||
colDdtypes[col.name.toUpperCase()] = getDdType(col)
|
||||
|
||||
fmt_stagetable = stageData.rows.map(r => {
|
||||
const fmt = {}
|
||||
for (const key of Object.keys(r)) {
|
||||
const ddtype = colDdtypes[key.toUpperCase()]
|
||||
if (ddtype === 'DATE' || ddtype === 'DATETIME' || ddtype === 'TIME') {
|
||||
fmt[key] = formatSasValue(r[key], ddtype)
|
||||
} else {
|
||||
fmt[key] = String(r[key])
|
||||
}
|
||||
}
|
||||
return fmt
|
||||
})
|
||||
} catch(err) {
|
||||
console.log('Error loading staged data: ' + err.message)
|
||||
fmt_stagetable = stageData.rows.map((r) => {
|
||||
const fmt = {}
|
||||
for (const key of Object.keys(r)) {
|
||||
const ddtype = colDdtypes[key.toUpperCase()]
|
||||
if (ddtype === 'DATE' || ddtype === 'DATETIME' || ddtype === 'TIME') {
|
||||
fmt[key] = formatSasValue(r[key], ddtype)
|
||||
} else {
|
||||
fmt[key] = String(r[key])
|
||||
}
|
||||
}
|
||||
return fmt
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error loading staged data: ' + err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webOutOpen()
|
||||
|
||||
@@ -1,110 +1,689 @@
|
||||
_webout = `{"SYSDATE" : "07OCT22"
|
||||
,"SYSTIME" : "12:25"
|
||||
, "params":
|
||||
[
|
||||
{"TABLE_ID":"DC20221007T122326121_612316_7259" ,"SUBMIT_STATUS_CD":"SUBMITTED" ,"BASE_LIB":"DC_JSLIB" ,"BASE_DS":"MPE_X_TEST" ,"SUBMITTED_BY_NM":"mihajlo" ,"SUBMITTED_ON_DTTM":1980764606.22 ,"SUBMITTED_REASON_TXT":"" ,"INPUT_OBS":15 ,"INPUT_VARS":10 ,"NUM_OF_APPROVALS_REQUIRED":1 ,"NUM_OF_APPROVALS_REMAINING":1 ,"REVIEWED_BY_NM":"" ,"REVIEWED_ON_DTTM":null ,"LIBDS":"DC_JSLIB.MPE_X_TEST" ,"REVIEWED_ON":"." ,"DIFFTIME":"07OCT22:12:25:57.40" ,"DIFFS_CSV":"tempDiffs_20221007_122559.csv" ,"FILESIZE":"684.0KB" ,"FILESIZE_RAW":700416 ,"TRUNCATED":"NO" ,"NUM_ADDED":6 ,"NUM_DELETED":4 ,"NUM_UPDATED":5 ,"SUBMITTED_ON":"07OCT2022:12:23:26" ,"ISAPPROVER":"YES" }
|
||||
]
|
||||
, "cols":
|
||||
[
|
||||
{"NAME":"PRIMARY_KEY_FIELD" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"" }
|
||||
,{"NAME":"SOME_CHAR" ,"TYPE":2 ,"LENGTH":32767 ,"FORMAT":"" }
|
||||
,{"NAME":"SOME_DROPDOWN" ,"TYPE":2 ,"LENGTH":128 ,"FORMAT":"" }
|
||||
,{"NAME":"SOME_NUM" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"" }
|
||||
,{"NAME":"SOME_DATE" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"DATE" }
|
||||
,{"NAME":"SOME_DATETIME" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"DATETIME" }
|
||||
,{"NAME":"SOME_TIME" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"TIME" }
|
||||
,{"NAME":"SOME_SHORTNUM" ,"TYPE":1 ,"LENGTH":4 ,"FORMAT":"" }
|
||||
,{"NAME":"SOME_BESTNUM" ,"TYPE":1 ,"LENGTH":8 ,"FORMAT":"BEST" }
|
||||
]
|
||||
, "submits":
|
||||
[
|
||||
{"TABLE_ID":"DC20221007T122119628_344949_7254" ,"SUBMIT_STATUS_CD":"SUBMITTED" ,"BASE_LIB":"DC_JSLIB" ,"BASE_DS":"MPE_X_TEST" ,"SUBMITTED_BY_NM":"mihajlo" ,"SUBMITTED_ON_DTTM":1980764479.74 ,"SUBMITTED_REASON_TXT":"" ,"INPUT_OBS":15 ,"INPUT_VARS":10 ,"NUM_OF_APPROVALS_REQUIRED":1 ,"NUM_OF_APPROVALS_REMAINING":1 ,"REVIEWED_BY_NM":"" ,"REVIEWED_ON_DTTM":null }
|
||||
,{"TABLE_ID":"DC20221006T142649516_059582_7169" ,"SUBMIT_STATUS_CD":"SUBMITTED" ,"BASE_LIB":"DC_JSLIB" ,"BASE_DS":"MPE_X_TEST" ,"SUBMITTED_BY_NM":"mihajlo" ,"SUBMITTED_ON_DTTM":1980685609.6 ,"SUBMITTED_REASON_TXT":"" ,"INPUT_OBS":1 ,"INPUT_VARS":10 ,"NUM_OF_APPROVALS_REQUIRED":1 ,"NUM_OF_APPROVALS_REMAINING":1 ,"REVIEWED_BY_NM":"" ,"REVIEWED_ON_DTTM":null }
|
||||
]
|
||||
, "deleted":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":1016 ,"SOME_CHAR":"16 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.6963824517 ,"SOME_DATE":790 ,"SOME_DATETIME":null ,"SOME_TIME":60 ,"SOME_SHORTNUM":27 ,"SOME_BESTNUM":84 }
|
||||
,{"PRIMARY_KEY_FIELD":1017 ,"SOME_CHAR":"17 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.476432963 ,"SOME_DATE":432 ,"SOME_DATETIME":null ,"SOME_TIME":60 ,"SOME_SHORTNUM":6 ,"SOME_BESTNUM":93 }
|
||||
,{"PRIMARY_KEY_FIELD":1018 ,"SOME_CHAR":"18 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.498669565 ,"SOME_DATE":754 ,"SOME_DATETIME":null ,"SOME_TIME":0 ,"SOME_SHORTNUM":87 ,"SOME_BESTNUM":29 }
|
||||
,{"PRIMARY_KEY_FIELD":1019 ,"SOME_CHAR":"19 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.7999903256 ,"SOME_DATE":492 ,"SOME_DATETIME":null ,"SOME_TIME":60 ,"SOME_SHORTNUM":4 ,"SOME_BESTNUM":15 }
|
||||
]
|
||||
, "new":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":10101234 ,"SOME_CHAR":"10 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.3297298762 ,"SOME_DATE":728 ,"SOME_DATETIME":null ,"SOME_TIME":60 ,"SOME_SHORTNUM":52 ,"SOME_BESTNUM":23 }
|
||||
,{"PRIMARY_KEY_FIELD":10101235 ,"SOME_CHAR":"11 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4249602912 ,"SOME_DATE":456 ,"SOME_DATETIME":null ,"SOME_TIME":60 ,"SOME_SHORTNUM":37 ,"SOME_BESTNUM":91 }
|
||||
,{"PRIMARY_KEY_FIELD":10101236 ,"SOME_CHAR":"12 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.5175091785 ,"SOME_DATE":383 ,"SOME_DATETIME":null ,"SOME_TIME":0 ,"SOME_SHORTNUM":53 ,"SOME_BESTNUM":5 }
|
||||
,{"PRIMARY_KEY_FIELD":10101237 ,"SOME_CHAR":"13 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.1282490171 ,"SOME_DATE":650 ,"SOME_DATETIME":20160 ,"SOME_TIME":0 ,"SOME_SHORTNUM":74 ,"SOME_BESTNUM":88 }
|
||||
,{"PRIMARY_KEY_FIELD":10101238 ,"SOME_CHAR":"14 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.9579004319 ,"SOME_DATE":203 ,"SOME_DATETIME":20160 ,"SOME_TIME":0 ,"SOME_SHORTNUM":51 ,"SOME_BESTNUM":65 }
|
||||
,{"PRIMARY_KEY_FIELD":10101239 ,"SOME_CHAR":"15 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":0.4804529285 ,"SOME_DATE":155 ,"SOME_DATETIME":23820 ,"SOME_TIME":0 ,"SOME_SHORTNUM":28 ,"SOME_BESTNUM":88 }
|
||||
]
|
||||
, "updates":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":0 ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":null ,"SOME_TIME":0 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":1 ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":null ,"SOME_TIME":420 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":2 ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":null ,"SOME_TIME":120 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":3 ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":1613.001 ,"SOME_DATE":423 ,"SOME_DATETIME":null ,"SOME_TIME":0 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":4 ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":1613.0011235 ,"SOME_DATE":4231 ,"SOME_DATETIME":null ,"SOME_TIME":360 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
]
|
||||
, "originals":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":0 ,"SOME_CHAR":"this is dummy data" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":42 ,"SOME_TIME":42 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":1 ,"SOME_CHAR":"more dummy data" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":42 ,"SOME_TIME":422 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":2 ,"SOME_CHAR":"even more dummy data" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":42 ,"SOME_DATE":42 ,"SOME_DATETIME":42 ,"SOME_TIME":142 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":3 ,"SOME_CHAR":"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":1613.001 ,"SOME_DATE":423 ,"SOME_DATETIME":423 ,"SOME_TIME":44 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
,{"PRIMARY_KEY_FIELD":4 ,"SOME_CHAR":"if you can fill the unforgiving minute" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":1613.001123456 ,"SOME_DATE":4231 ,"SOME_DATETIME":423123123 ,"SOME_TIME":412 ,"SOME_SHORTNUM":3 ,"SOME_BESTNUM":44 }
|
||||
]
|
||||
, "fmt_deleted":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"1016" ,"SOME_CHAR":"16 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.6963824517" ,"SOME_DATE":"01MAR1962" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:01:00" ,"SOME_SHORTNUM":"27" ,"SOME_BESTNUM":"84" }
|
||||
,{"PRIMARY_KEY_FIELD":"1017" ,"SOME_CHAR":"17 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.476432963" ,"SOME_DATE":"08MAR1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:01:00" ,"SOME_SHORTNUM":"6" ,"SOME_BESTNUM":"93" }
|
||||
,{"PRIMARY_KEY_FIELD":"1018" ,"SOME_CHAR":"18 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.498669565" ,"SOME_DATE":"24JAN1962" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"87" ,"SOME_BESTNUM":"29" }
|
||||
,{"PRIMARY_KEY_FIELD":"1019" ,"SOME_CHAR":"19 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.7999903256" ,"SOME_DATE":"07MAY1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:01:00" ,"SOME_SHORTNUM":"4" ,"SOME_BESTNUM":"15" }
|
||||
]
|
||||
, "fmt_new":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"10101234" ,"SOME_CHAR":"10 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.3297298762" ,"SOME_DATE":"29DEC1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:01:00" ,"SOME_SHORTNUM":"52" ,"SOME_BESTNUM":"23" }
|
||||
,{"PRIMARY_KEY_FIELD":"10101235" ,"SOME_CHAR":"11 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.4249602912" ,"SOME_DATE":"01APR1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:01:00" ,"SOME_SHORTNUM":"37" ,"SOME_BESTNUM":"91" }
|
||||
,{"PRIMARY_KEY_FIELD":"10101236" ,"SOME_CHAR":"12 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.5175091785" ,"SOME_DATE":"18JAN1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"53" ,"SOME_BESTNUM":"5" }
|
||||
,{"PRIMARY_KEY_FIELD":"10101237" ,"SOME_CHAR":"13 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.1282490171" ,"SOME_DATE":"12OCT1961" ,"SOME_DATETIME":"01JAN1960:05:36:00" ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"74" ,"SOME_BESTNUM":"88" }
|
||||
,{"PRIMARY_KEY_FIELD":"10101238" ,"SOME_CHAR":"14 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.9579004319" ,"SOME_DATE":"22JUL1960" ,"SOME_DATETIME":"01JAN1960:05:36:00" ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"51" ,"SOME_BESTNUM":"65" }
|
||||
,{"PRIMARY_KEY_FIELD":"10101239" ,"SOME_CHAR":"15 bottles of beer on the wall" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"0.4804529285" ,"SOME_DATE":"04JUN1960" ,"SOME_DATETIME":"01JAN1960:06:37:00" ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"28" ,"SOME_BESTNUM":"88" }
|
||||
]
|
||||
, "fmt_updates":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"0" ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"1" ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:07:00" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"2" ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:02:00" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"3" ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":"1613.001" ,"SOME_DATE":"27FEB1961" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:00:00" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"4" ,"SOME_CHAR":"updated row" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"1613.0011235" ,"SOME_DATE":"02AUG1971" ,"SOME_DATETIME":"." ,"SOME_TIME":"0:06:00" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
]
|
||||
, "fmt_originals":
|
||||
[
|
||||
{"PRIMARY_KEY_FIELD":"0" ,"SOME_CHAR":"this is dummy data" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"01JAN1960:00:00:42" ,"SOME_TIME":"0:00:42" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"1" ,"SOME_CHAR":"more dummy data" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"01JAN1960:00:00:42" ,"SOME_TIME":"0:07:02" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"2" ,"SOME_CHAR":"even more dummy data" ,"SOME_DROPDOWN":"Option 3" ,"SOME_NUM":"42" ,"SOME_DATE":"12FEB1960" ,"SOME_DATETIME":"01JAN1960:00:00:42" ,"SOME_TIME":"0:02:22" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"3" ,"SOME_CHAR":"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:" ,"SOME_DROPDOWN":"Option 2" ,"SOME_NUM":"1613.001" ,"SOME_DATE":"27FEB1961" ,"SOME_DATETIME":"01JAN1960:00:07:03" ,"SOME_TIME":"0:00:44" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
,{"PRIMARY_KEY_FIELD":"4" ,"SOME_CHAR":"if you can fill the unforgiving minute" ,"SOME_DROPDOWN":"Option 1" ,"SOME_NUM":"1613.0011235" ,"SOME_DATE":"02AUG1971" ,"SOME_DATETIME":"29MAY1973:06:12:03" ,"SOME_TIME":"0:06:52" ,"SOME_SHORTNUM":"3" ,"SOME_BESTNUM":"44" }
|
||||
]
|
||||
,"_DEBUG" : ""
|
||||
,"_PROGRAM" : "/30.SASApps/app/mihajlo/services/auditors/postdata"
|
||||
,"AUTOEXEC" : "%2Fhome%2Fmihajlo%2Fsasjs_root%2Fsessions%2F20221007122541-37053-1665145541605%2Fautoexec.sas"
|
||||
,"MF_GETUSER" : "mihajlo"
|
||||
,"SYSCC" : "0"
|
||||
,"SYSENCODING" : "utf-8"
|
||||
,"SYSERRORTEXT" : ""
|
||||
,"SYSHOSTINFOLONG" : ""
|
||||
,"SYSHOSTNAME" : "sas.4gl.io"
|
||||
,"SYSPROCESSID" : "41DD84049178EB530000000000000000"
|
||||
,"SYSPROCESSMODE" : "Stored Program"
|
||||
,"SYSPROCESSNAME" : ""
|
||||
,"SYSJOBID" : "726676"
|
||||
,"SYSSCPL" : "LINUX"
|
||||
,"syssite" : "123"
|
||||
,"SYSTCPIPHOSTNAME" : "https://sas.4gl.io:5002"
|
||||
,"SYSUSERID" : "mihajlo"
|
||||
,"SYSVLONG" : "05.00.00.02.001146"
|
||||
,"SYSWARNINGTEXT" : ""
|
||||
,"END_DTTM" : "2022-10-07T12:26:00.209420"
|
||||
,"MEMSIZE" : "0KB"
|
||||
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'
|
||||
const dataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref)
|
||||
const stagingDir = nodePath.resolve(dataDir, 'staging')
|
||||
|
||||
eval(
|
||||
fs.readFileSync(
|
||||
nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
const loadTableData = makeTableLoader(dataDir)
|
||||
|
||||
function saveTableData(tableName, tableData) {
|
||||
const file = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
|
||||
fs.writeFileSync(file, JSON.stringify(tableData, null, 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the staging subfolder for a given table_id, creating it if needed.
|
||||
* Each submission gets its own subfolder under staging/ so that staged data
|
||||
* can sit alongside audit artifacts (submit/approval logs, temp diffs, etc).
|
||||
*/
|
||||
function getStageFolder(tableId) {
|
||||
const folder = nodePath.resolve(stagingDir, tableId)
|
||||
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true })
|
||||
return folder
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads staged data for a table_id from the subfolder layout, with a fallback
|
||||
* to the legacy flat-file layout (staging/<table_id>.json).
|
||||
*/
|
||||
function loadStagedData(tableId) {
|
||||
const candidates = [
|
||||
nodePath.resolve(stagingDir, tableId, 'staged.json'),
|
||||
nodePath.resolve(stagingDir, tableId + '.json')
|
||||
]
|
||||
for (const f of candidates) {
|
||||
if (fs.existsSync(f)) {
|
||||
return JSON.parse(fs.readFileSync(f, { encoding: 'utf8' }).toString())
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies audit items from the Stored Program's session directory (its
|
||||
* current working directory at runtime) into the staging subfolder.
|
||||
* - log.log -> <prefix>.log
|
||||
* - code.js -> <prefix>_code.js
|
||||
* - reqHeaders.txt -> <prefix>_reqHeaders.txt
|
||||
*/
|
||||
function copySessionAuditItems(stageFolder, prefix) {
|
||||
try {
|
||||
const sessionDir = nodePath.dirname(weboutPath)
|
||||
const copies = [
|
||||
['log.log', prefix + '.log'],
|
||||
['code.js', prefix + '_code.js'],
|
||||
['reqHeaders.txt', prefix + '_reqHeaders.txt']
|
||||
]
|
||||
for (const [src, dest] of copies) {
|
||||
const srcPath = nodePath.resolve(sessionDir, src)
|
||||
if (fs.existsSync(srcPath)) {
|
||||
fs.copyFileSync(srcPath, nodePath.resolve(stageFolder, dest))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error copying session audit items: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Parse input (SASCONTROLTABLE: ACTION / TABLE / DIFFTIME) ────────────────
|
||||
|
||||
const _sctRow = fetchTable('SASControlTable')[0] || {}
|
||||
const action = _sctRow.ACTION || ''
|
||||
const loadRef = _sctRow.TABLE || _sctRow.table || ''
|
||||
|
||||
// ─── Load submission from MPE_SUBMIT ─────────────────────────────────────────
|
||||
|
||||
const submitData = loadTableData('MPE_SUBMIT') || { rows: [] }
|
||||
const submitRow = submitData.rows.find((r) => r.TABLE_ID === loadRef) || {}
|
||||
|
||||
const libref = submitRow.BASE_LIB || dcLibref
|
||||
const dsn = submitRow.BASE_DS || ''
|
||||
|
||||
// ─── Load table registration from MPE_TABLES (BUSKEY, LOADTYPE, temporal vars)
|
||||
|
||||
const mpeTablesData = loadTableData('MPE_TABLES') || { rows: [] }
|
||||
const tableReg =
|
||||
mpeTablesData.rows.find(
|
||||
(r) => (r.libref || r.LIBREF) === libref && (r.dsn || r.DSN) === dsn
|
||||
) || {}
|
||||
|
||||
const buskey = String(tableReg.buskey || tableReg.BUSKEY || '')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
const loadtype = tableReg.loadtype || tableReg.LOADTYPE || 'UPDATE'
|
||||
const varTxfrom = tableReg.var_txfrom || tableReg.VAR_TXFROM || ''
|
||||
const varTxto = tableReg.var_txto || tableReg.VAR_TXTO || ''
|
||||
const varBusfrom = tableReg.var_busfrom || tableReg.VAR_BUSFROM || ''
|
||||
const varBusto = tableReg.var_busto || tableReg.VAR_BUSTO || ''
|
||||
const numApprovalsRequired =
|
||||
submitRow.NUM_OF_APPROVALS_REQUIRED || tableReg.num_of_approvals_required || 1
|
||||
const numApprovalsRemaining =
|
||||
submitRow.NUM_OF_APPROVALS_REMAINING || numApprovalsRequired
|
||||
|
||||
// ─── Load base table and staged data ─────────────────────────────────────────
|
||||
|
||||
const baseTableData = loadTableData(dsn) || { columns: [], rows: [] }
|
||||
const baseCols = (baseTableData.columns || []).map((c) => ({
|
||||
...c,
|
||||
name: String(c.name).toUpperCase()
|
||||
}))
|
||||
const baseRows = baseTableData.rows || []
|
||||
|
||||
let stagedRows = []
|
||||
const stageData = loadStagedData(loadRef)
|
||||
if (stageData) {
|
||||
stagedRows = stageData.rows || []
|
||||
}
|
||||
|
||||
// ─── Column setup ─────────────────────────────────────────────────────────────
|
||||
// Diff is displayed on non-temporal columns only (tech + business dates are
|
||||
// managed by the loader, as in %quickmacro in postdata.sas)
|
||||
|
||||
const excludeCols = new Set(
|
||||
[varTxfrom, varTxto, varBusfrom, varBusto]
|
||||
.filter(Boolean)
|
||||
.map((v) => String(v).toUpperCase())
|
||||
)
|
||||
const diffCols = baseCols.filter((c) => !excludeCols.has(c.name))
|
||||
const colDdtypes = {}
|
||||
for (const col of diffCols) colDdtypes[col.name] = getDdType(col)
|
||||
|
||||
// ─── Value normalisation ──────────────────────────────────────────────────────
|
||||
// Staged values arrive from CSV as strings ("42"), base values are native JSON
|
||||
// (42). Compare numerics by value and strings case-sensitively.
|
||||
|
||||
function normVal(value, colName) {
|
||||
const col = diffCols.find((c) => c.name === colName)
|
||||
if (col && col.type === 'N') {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const num = Number(value)
|
||||
return isNaN(num) ? null : num
|
||||
}
|
||||
if (value === null || value === undefined) return null
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function rowKeyOf(row) {
|
||||
return buskey
|
||||
.map((k) => {
|
||||
const actual = Object.keys(row).find(
|
||||
(kk) => kk.toUpperCase() === k.toUpperCase()
|
||||
)
|
||||
return normVal(row[actual], k)
|
||||
})
|
||||
.join('|')
|
||||
}
|
||||
|
||||
function stripRow(row) {
|
||||
// rebuild in base table column order, dropping the delete flag and any
|
||||
// staged column not present in the base table (mirrors the SAS data step
|
||||
// "if 0 then set &emptybasetable; set &inds;" in %quickmacro)
|
||||
const out = {}
|
||||
for (const col of diffCols) {
|
||||
const actual = Object.keys(row).find((k) => k.toUpperCase() === col.name)
|
||||
if (actual === undefined) {
|
||||
out[col.name] = null
|
||||
} else if (col.type === 'N') {
|
||||
out[col.name] = normVal(row[actual], col.name)
|
||||
} else {
|
||||
out[col.name] = row[actual] === undefined ? null : String(row[actual])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ─── Compute the diff (mirrors %mpe_targetloader -> %bitemporal_dataloader) ──
|
||||
|
||||
const DELETE_FLAG = '_____DELETE__THIS__RECORD_____'
|
||||
|
||||
const deleted = []
|
||||
const newRows = []
|
||||
const updatedRows = []
|
||||
const originals = []
|
||||
|
||||
if (loadtype === 'REPLACE') {
|
||||
// full replace: every staged record is NEW and every previous base
|
||||
// record is DELETED (mirrors the LOADTARGET=NO REPLACE branch of
|
||||
// %mpe_targetloader)
|
||||
for (const row of stagedRows) newRows.push(stripRow(row))
|
||||
for (const row of baseRows) deleted.push(stripRow(row))
|
||||
} else {
|
||||
// index base rows by buskey
|
||||
const baseByBuskey = {}
|
||||
for (const row of baseRows) {
|
||||
baseByBuskey[rowKeyOf(row)] = row
|
||||
}
|
||||
|
||||
for (const row of stagedRows) {
|
||||
const stripped = stripRow(row)
|
||||
const key = rowKeyOf(row)
|
||||
const deleteFlagRaw = Object.keys(row).find(
|
||||
(k) => k.toUpperCase() === DELETE_FLAG
|
||||
)
|
||||
const isDelete =
|
||||
deleteFlagRaw && String(row[deleteFlagRaw]).trim().toUpperCase() === 'YES'
|
||||
|
||||
if (isDelete) {
|
||||
const baseRow = baseByBuskey[key]
|
||||
if (baseRow) deleted.push(stripRow(baseRow))
|
||||
continue
|
||||
}
|
||||
|
||||
const baseRow = baseByBuskey[key]
|
||||
if (!baseRow) {
|
||||
newRows.push(stripped)
|
||||
continue
|
||||
}
|
||||
|
||||
// compare non-key values
|
||||
const strippedBase = stripRow(baseRow)
|
||||
let changed = false
|
||||
for (const col of diffCols) {
|
||||
const name = col.name
|
||||
if (buskey.includes(name)) continue
|
||||
if (String(stripped[name]) !== String(strippedBase[name])) {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
updatedRows.push(stripped)
|
||||
originals.push(strippedBase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Sort by buskey (SAS orders diffs by business key) ────────────────────────
|
||||
|
||||
function sortRows(rows) {
|
||||
return rows.slice().sort((a, b) => {
|
||||
for (const k of buskey) {
|
||||
const av = a[k],
|
||||
bv = b[k]
|
||||
if (av === bv) continue
|
||||
if (typeof av === 'number' && typeof bv === 'number') return av - bv
|
||||
return String(av) < String(bv) ? -1 : 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
const sortedDeleted = sortRows(deleted)
|
||||
const sortedNew = sortRows(newRows)
|
||||
const sortedUpdates = sortRows(updatedRows)
|
||||
const sortedOriginals = sortRows(originals)
|
||||
|
||||
// ─── maxdiff truncation (same as &maxdiff=100 in postdata.sas) ───────────────
|
||||
|
||||
const maxdiff = 100
|
||||
|
||||
// ─── cols table (proc contents of work.updates) ───────────────────────────────
|
||||
|
||||
const cols = diffCols.map((col) => {
|
||||
const fmtname = (col.format || '')
|
||||
.replace(/[\d.]+$/, '')
|
||||
.replace('datetime', 'DATETIME')
|
||||
.replace('date', 'DATE')
|
||||
.replace('time', 'TIME')
|
||||
.replace('best', 'BEST')
|
||||
return {
|
||||
NAME: col.name,
|
||||
TYPE: col.type === 'N' ? 1 : 2,
|
||||
LENGTH: col.length,
|
||||
FORMAT: col.type === 'N' ? fmtname.toUpperCase() : ''
|
||||
}
|
||||
})
|
||||
|
||||
// ─── formatted variants (fmt_* tables in %webout) ─────────────────────────────
|
||||
|
||||
function fmtRow(row) {
|
||||
const out = {}
|
||||
for (const key of Object.keys(row)) {
|
||||
const ddtype = colDdtypes[key]
|
||||
if (ddtype === 'DATE' || ddtype === 'DATETIME' || ddtype === 'TIME') {
|
||||
out[key] = formatSasValue(row[key], ddtype)
|
||||
} else if (row[key] === null || row[key] === undefined) {
|
||||
out[key] = ''
|
||||
} else {
|
||||
out[key] = String(row[key])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ─── other submits against the same base table (max 10) ──────────────────────
|
||||
|
||||
const submits = submitData.rows
|
||||
.filter(
|
||||
(r) =>
|
||||
r.SUBMIT_STATUS_CD === 'SUBMITTED' &&
|
||||
r.BASE_LIB === libref &&
|
||||
r.BASE_DS === dsn &&
|
||||
r.TABLE_ID !== loadRef
|
||||
)
|
||||
.sort((a, b) => b.SUBMITTED_ON_DTTM - a.SUBMITTED_ON_DTTM)
|
||||
.slice(0, 10)
|
||||
|
||||
// ─── TEMPDIFFS csv (kept in the staging subfolder for audit) ──────────────────
|
||||
|
||||
function sasDateTimeNow() {
|
||||
const d = new Date()
|
||||
const pad = (n, w) => String(n).padStart(w || 2, '0')
|
||||
return (
|
||||
d.getFullYear() +
|
||||
pad(d.getMonth() + 1) +
|
||||
pad(d.getDate()) +
|
||||
'_' +
|
||||
pad(d.getHours()) +
|
||||
pad(d.getMinutes()) +
|
||||
pad(d.getSeconds())
|
||||
)
|
||||
}
|
||||
|
||||
// ─── helpers for datetime formatting ─────────────────────────────────────────
|
||||
|
||||
function sasDt(d) {
|
||||
if (!d) return '.'
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
const MONTHS = [
|
||||
'JAN',
|
||||
'FEB',
|
||||
'MAR',
|
||||
'APR',
|
||||
'MAY',
|
||||
'JUN',
|
||||
'JUL',
|
||||
'AUG',
|
||||
'SEP',
|
||||
'OCT',
|
||||
'NOV',
|
||||
'DEC'
|
||||
]
|
||||
return (
|
||||
pad(d.getDate()) +
|
||||
MONTHS[d.getMonth()] +
|
||||
d.getFullYear() +
|
||||
':' +
|
||||
pad(d.getHours()) +
|
||||
':' +
|
||||
pad(d.getMinutes()) +
|
||||
':' +
|
||||
pad(d.getSeconds())
|
||||
)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const sastime = now.getTime() / 1000
|
||||
const userNm =
|
||||
typeof _METAUSER !== 'undefined' && _METAUSER ? _METAUSER : 'sasdemo'
|
||||
|
||||
// ─── APPLY THE LOAD (used by both SHOW_DIFFS temp-table creation and
|
||||
// APPROVE_TABLE actual load) ──────────────────────────────────────────────
|
||||
// For UPDATE / TXTEMPORAL / BITEMPORAL: upsert staged rows into the base table
|
||||
// (new rows appended, updated rows replaced, deleted rows removed).
|
||||
// For REPLACE: wipe the base table and append all staged rows.
|
||||
// For FORMAT_CAT: not supported in the mock (no format catalog engine).
|
||||
|
||||
function applyLoadToBase() {
|
||||
const updatedBaseRows = baseRows.map((r) => ({ ...r }))
|
||||
|
||||
if (loadtype === 'REPLACE') {
|
||||
// replace all base rows with staged rows (minus delete flag / temporal cols)
|
||||
return stagedRows.map(stripRow)
|
||||
}
|
||||
|
||||
// index base rows by buskey for quick lookup
|
||||
const baseByBuskey = {}
|
||||
for (let i = 0; i < updatedBaseRows.length; i++) {
|
||||
baseByBuskey[rowKeyOf(updatedBaseRows[i])] = i
|
||||
}
|
||||
|
||||
for (const row of stagedRows) {
|
||||
const stripped = stripRow(row)
|
||||
const key = rowKeyOf(row)
|
||||
const deleteFlagRaw = Object.keys(row).find(
|
||||
(k) => k.toUpperCase() === DELETE_FLAG
|
||||
)
|
||||
const isDelete =
|
||||
deleteFlagRaw && String(row[deleteFlagRaw]).trim().toUpperCase() === 'YES'
|
||||
|
||||
if (isDelete) {
|
||||
const idx = baseByBuskey[key]
|
||||
if (idx !== undefined) {
|
||||
updatedBaseRows.splice(idx, 1)
|
||||
// rebuild index after splice
|
||||
for (let i = 0; i < updatedBaseRows.length; i++) {
|
||||
baseByBuskey[rowKeyOf(updatedBaseRows[i])] = i
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const idx = baseByBuskey[key]
|
||||
if (idx !== undefined) {
|
||||
updatedBaseRows[idx] = stripped
|
||||
} else {
|
||||
updatedBaseRows.push(stripped)
|
||||
baseByBuskey[key] = updatedBaseRows.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
return updatedBaseRows
|
||||
}
|
||||
|
||||
// ─── Route by action ─────────────────────────────────────────────────────────
|
||||
|
||||
if (action === 'SHOW_DIFFS') {
|
||||
// ── SHOW_DIFFS: build diff tables for the approval screen ──────────────────
|
||||
|
||||
const stageFolder = getStageFolder(loadRef)
|
||||
const tempDiffsCsv = 'tempDiffs_' + sasDateTimeNow() + '.csv'
|
||||
|
||||
try {
|
||||
const csvLines = ['_____status,' + diffCols.map((c) => c.name).join(',')]
|
||||
const csvEscape = (v) =>
|
||||
v === null || v === undefined
|
||||
? ''
|
||||
: '"' + String(v).replace(/"/g, '""') + '"'
|
||||
const pushRows = (rows, status) => {
|
||||
for (const row of rows) {
|
||||
csvLines.push(
|
||||
csvEscape(status) +
|
||||
',' +
|
||||
diffCols.map((c) => csvEscape(row[c.name])).join(',')
|
||||
)
|
||||
}
|
||||
}
|
||||
pushRows(sortedDeleted, 'DELETED ')
|
||||
pushRows(sortedNew, 'NEW')
|
||||
pushRows(sortedUpdates, 'UPDATED')
|
||||
pushRows(sortedOriginals, 'ORIGINAL')
|
||||
fs.writeFileSync(
|
||||
nodePath.resolve(stageFolder, tempDiffsCsv),
|
||||
csvLines.join('\n')
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('Error writing TEMPDIFFS csv: ' + err.message)
|
||||
}
|
||||
|
||||
let filesizeRaw = 0
|
||||
try {
|
||||
filesizeRaw = fs.statSync(nodePath.resolve(stageFolder, tempDiffsCsv)).size
|
||||
} catch (err) {
|
||||
/* csv not written */
|
||||
}
|
||||
|
||||
const submittedOn = submitRow.SUBMITTED_ON_DTTM
|
||||
? new Date(submitRow.SUBMITTED_ON_DTTM * 1000)
|
||||
: null
|
||||
|
||||
const truncated =
|
||||
sortedOriginals.length > maxdiff ||
|
||||
sortedNew.length > maxdiff ||
|
||||
sortedDeleted.length > maxdiff ||
|
||||
sortedUpdates.length > maxdiff
|
||||
|
||||
const params = [
|
||||
{
|
||||
TABLE_ID: submitRow.TABLE_ID,
|
||||
SUBMIT_STATUS_CD: submitRow.SUBMIT_STATUS_CD,
|
||||
BASE_LIB: libref,
|
||||
BASE_DS: dsn,
|
||||
SUBMITTED_BY_NM: submitRow.SUBMITTED_BY_NM || '',
|
||||
SUBMITTED_ON_DTTM: submitRow.SUBMITTED_ON_DTTM,
|
||||
SUBMITTED_REASON_TXT: submitRow.SUBMITTED_REASON_TXT || '',
|
||||
INPUT_OBS: submitRow.INPUT_OBS,
|
||||
INPUT_VARS: submitRow.INPUT_VARS,
|
||||
NUM_OF_APPROVALS_REQUIRED: numApprovalsRequired,
|
||||
NUM_OF_APPROVALS_REMAINING: numApprovalsRemaining,
|
||||
REVIEWED_BY_NM: submitRow.REVIEWED_BY_NM || '',
|
||||
REVIEWED_ON_DTTM: submitRow.REVIEWED_ON_DTTM || null,
|
||||
LIBDS: libref + '.' + dsn,
|
||||
REVIEWED_ON: submitRow.REVIEWED_ON_DTTM
|
||||
? sasDt(new Date(submitRow.REVIEWED_ON_DTTM * 1000))
|
||||
: '.',
|
||||
DIFFTIME: sasDt(now),
|
||||
DIFFS_CSV: tempDiffsCsv,
|
||||
FILESIZE: (filesizeRaw / 1024).toFixed(1) + 'KB',
|
||||
FILESIZE_RAW: filesizeRaw,
|
||||
TRUNCATED: truncated ? 'YES' : 'NO',
|
||||
NUM_ADDED: sortedNew.length,
|
||||
NUM_DELETED: sortedDeleted.length,
|
||||
NUM_UPDATED: sortedUpdates.length,
|
||||
SUBMITTED_ON: sasDt(submittedOn),
|
||||
ISAPPROVER: 'YES'
|
||||
}
|
||||
]
|
||||
|
||||
// already REVIEWED / APPROVED submissions just return params (mirrors postdata.sas)
|
||||
if (
|
||||
submitRow.SUBMIT_STATUS_CD === 'REJECTED' ||
|
||||
submitRow.SUBMIT_STATUS_CD === 'APPROVED'
|
||||
) {
|
||||
webOutOpen()
|
||||
webOutObj([submitRow], 'params')
|
||||
webOutClose()
|
||||
} else {
|
||||
webOutOpen()
|
||||
webOutObj(params, 'params')
|
||||
webOutObj(cols, 'cols')
|
||||
webOutObj(submits, 'submits')
|
||||
webOutObj(sortedDeleted.slice(0, maxdiff), 'deleted')
|
||||
webOutObj(sortedNew.slice(0, maxdiff), 'new')
|
||||
webOutObj(sortedUpdates.slice(0, maxdiff), 'updates')
|
||||
webOutObj(sortedOriginals, 'originals')
|
||||
webOutObj(sortedDeleted.slice(0, maxdiff).map(fmtRow), 'fmt_deleted')
|
||||
webOutObj(sortedNew.slice(0, maxdiff).map(fmtRow), 'fmt_new')
|
||||
webOutObj(sortedUpdates.slice(0, maxdiff).map(fmtRow), 'fmt_updates')
|
||||
webOutObj(sortedOriginals.map(fmtRow), 'fmt_originals')
|
||||
webOutClose()
|
||||
}
|
||||
} else if (action === 'APPROVE_TABLE') {
|
||||
// ── APPROVE_TABLE: apply the load to the base table and record the approval
|
||||
|
||||
const stageFolder = getStageFolder(loadRef)
|
||||
|
||||
// check: has this user already approved? (mirrors prev_upload_check in postdata.sas)
|
||||
const reviewData = loadTableData('MPE_REVIEW') || { rows: [] }
|
||||
const alreadyApproved = reviewData.rows.some(
|
||||
(r) =>
|
||||
r.TABLE_ID === loadRef &&
|
||||
r.REVIEWED_BY_NM === userNm &&
|
||||
r.REVIEW_STATUS_ID !== 'SUBMITTED'
|
||||
)
|
||||
|
||||
if (alreadyApproved) {
|
||||
webOutOpen()
|
||||
webOutObj([{ AUTHORISED: 1, PREV_UPLOAD_CHECK: 1 }], 'apPARAMS')
|
||||
webOutClose()
|
||||
} else if (numApprovalsRemaining > 1) {
|
||||
// multi-approval table: record this approval but don't load yet
|
||||
const apprNo = numApprovalsRequired - numApprovalsRemaining + 1
|
||||
|
||||
reviewData.rows.push({
|
||||
TABLE_ID: loadRef,
|
||||
BASE_TABLE: libref + '.' + dsn,
|
||||
REVIEW_STATUS_ID: 'APPROVED',
|
||||
REVIEWED_BY_NM: userNm,
|
||||
REVIEWED_ON_DTTM: sastime,
|
||||
REVIEW_REASON_TXT: 'APPROVAL ' + apprNo + ' of ' + numApprovalsRequired
|
||||
})
|
||||
saveTableData('MPE_REVIEW', reviewData)
|
||||
|
||||
// decrement remaining approvals in MPE_SUBMIT
|
||||
const subRow = submitData.rows.find((r) => r.TABLE_ID === loadRef)
|
||||
if (subRow) {
|
||||
subRow.NUM_OF_APPROVALS_REMAINING = numApprovalsRemaining - 1
|
||||
subRow.REVIEWED_BY_NM = userNm
|
||||
subRow.REVIEWED_ON_DTTM = sastime
|
||||
}
|
||||
saveTableData('MPE_SUBMIT', submitData)
|
||||
|
||||
// copy audit items from this session into the staging subfolder
|
||||
copySessionAuditItems(stageFolder, 'approve_' + apprNo)
|
||||
|
||||
const apReqd = [
|
||||
{
|
||||
AUTHORISED: 1,
|
||||
ALREADY_UPDATED: 0,
|
||||
ALREADY_UPDATED_DTTM: null,
|
||||
...submitRow
|
||||
}
|
||||
]
|
||||
|
||||
webOutOpen()
|
||||
webOutObj(apReqd, 'apReqd')
|
||||
webOutClose()
|
||||
} else {
|
||||
// ── final approval: apply the load to the base table ─────────────────────
|
||||
|
||||
// write the ActualDiffs csv (mirrors approve.ActualDiffs in postdata.sas)
|
||||
try {
|
||||
const csvLines = [
|
||||
'_____STATUS_____,' + diffCols.map((c) => c.name).join(',')
|
||||
]
|
||||
const csvEscape = (v) =>
|
||||
v === null || v === undefined
|
||||
? ''
|
||||
: '"' + String(v).replace(/"/g, '""') + '"'
|
||||
const pushRows = (rows, status) => {
|
||||
for (const row of rows) {
|
||||
csvLines.push(
|
||||
csvEscape(status) +
|
||||
',' +
|
||||
diffCols.map((c) => csvEscape(row[c.name])).join(',')
|
||||
)
|
||||
}
|
||||
}
|
||||
pushRows(sortedDeleted, 'DELETED')
|
||||
pushRows(sortedNew, 'NEW')
|
||||
pushRows(sortedUpdates, 'UPDATED')
|
||||
fs.writeFileSync(
|
||||
nodePath.resolve(stageFolder, 'ActualDiffs.csv'),
|
||||
csvLines.join('\n')
|
||||
)
|
||||
} catch (err) {
|
||||
console.log('Error writing ActualDiffs csv: ' + err.message)
|
||||
}
|
||||
|
||||
// apply the load to the base table
|
||||
const newBaseRows = applyLoadToBase()
|
||||
const updatedTableData = {
|
||||
name: baseTableData.name || dsn,
|
||||
columns: baseTableData.columns || baseCols,
|
||||
rows: newBaseRows
|
||||
}
|
||||
saveTableData(dsn, updatedTableData)
|
||||
|
||||
// record the approval in MPE_REVIEW
|
||||
const apprNo = numApprovalsRequired
|
||||
reviewData.rows.push({
|
||||
TABLE_ID: loadRef,
|
||||
BASE_TABLE: libref + '.' + dsn,
|
||||
REVIEW_STATUS_ID: 'APPROVED',
|
||||
REVIEWED_BY_NM: userNm,
|
||||
REVIEWED_ON_DTTM: sastime,
|
||||
REVIEW_REASON_TXT: 'APPROVAL ' + apprNo + ' of ' + numApprovalsRequired
|
||||
})
|
||||
saveTableData('MPE_REVIEW', reviewData)
|
||||
|
||||
// update MPE_SUBMIT to APPROVED
|
||||
const subRow = submitData.rows.find((r) => r.TABLE_ID === loadRef)
|
||||
if (subRow) {
|
||||
subRow.SUBMIT_STATUS_CD = 'APPROVED'
|
||||
subRow.NUM_OF_APPROVALS_REMAINING = 0
|
||||
subRow.REVIEWED_BY_NM = userNm
|
||||
subRow.REVIEWED_ON_DTTM = sastime
|
||||
}
|
||||
saveTableData('MPE_SUBMIT', submitData)
|
||||
|
||||
// record in MPE_DATALOADS (mirrors the dataloads entry in postdata.sas)
|
||||
const dataloadsData = loadTableData('MPE_DATALOADS') || { rows: [] }
|
||||
dataloadsData.rows.push({
|
||||
LIBREF: libref,
|
||||
DSN: dsn,
|
||||
ETLSOURCE: loadRef,
|
||||
LOADTYPE: loadtype,
|
||||
CHANGED_RECORDS: sortedUpdates.length,
|
||||
NEW_RECORDS: sortedNew.length,
|
||||
DELETED_RECORDS: sortedDeleted.length,
|
||||
DURATION: 0,
|
||||
USER_NM: userNm,
|
||||
PROCESSED_DTTM: sastime,
|
||||
MAC_VER: 'MOCK'
|
||||
})
|
||||
saveTableData('MPE_DATALOADS', dataloadsData)
|
||||
|
||||
// copy audit items from this session into the staging subfolder
|
||||
copySessionAuditItems(stageFolder, 'approve')
|
||||
|
||||
const apPARAMS = [
|
||||
{
|
||||
AUTHORISED: 1,
|
||||
ALREADY_UPDATED: 0,
|
||||
ALREADY_UPDATED_DTTM: null,
|
||||
DIFFTIME: _sctRow.DIFFTIME || sasDt(now),
|
||||
RESPONSE: 'SUCCESS!'
|
||||
}
|
||||
]
|
||||
|
||||
webOutOpen()
|
||||
webOutObj(apPARAMS, 'apPARAMS')
|
||||
webOutClose()
|
||||
}
|
||||
} else {
|
||||
// unsupported action
|
||||
webOutOpen()
|
||||
webOutObj([], 'params')
|
||||
webOutClose()
|
||||
}
|
||||
`
|
||||
@@ -158,11 +158,11 @@ function formatSasValue(value, ddtype) {
|
||||
|
||||
if (ddtype === 'DATE') {
|
||||
const d = new Date(SAS_EPOCH_MS + value * 86400000)
|
||||
return `${d.getUTCDate()}${MONTHS_ABBR[d.getUTCMonth()]}${d.getUTCFullYear()}`
|
||||
return `${String(d.getUTCDate()).padStart(2,'0')}${MONTHS_ABBR[d.getUTCMonth()]}${d.getUTCFullYear()}`
|
||||
}
|
||||
if (ddtype === 'DATETIME') {
|
||||
const d = new Date(SAS_EPOCH_MS + value * 1000)
|
||||
return `${d.getUTCDate()}${MONTHS_ABBR[d.getUTCMonth()]}${d.getUTCFullYear()}:${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}:${String(d.getUTCSeconds()).padStart(2,'0')}`
|
||||
return `${String(d.getUTCDate()).padStart(2,'0')}${MONTHS_ABBR[d.getUTCMonth()]}${d.getUTCFullYear()}:${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}:${String(d.getUTCSeconds()).padStart(2,'0')}`
|
||||
}
|
||||
if (ddtype === 'TIME') {
|
||||
const h = Math.floor(value / 3600)
|
||||
|
||||
@@ -7,15 +7,44 @@ const dcLibref = 'DC_JSLIB'
|
||||
const dataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref)
|
||||
const stagingDir = nodePath.resolve(dataDir, 'staging')
|
||||
|
||||
if (!fs.existsSync(stagingDir)) fs.mkdirSync(stagingDir, { recursive: true })
|
||||
|
||||
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
||||
eval(
|
||||
fs.readFileSync(
|
||||
nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
const loadTableData = makeTableLoader(dataDir)
|
||||
|
||||
function saveTableData(tableName, tableData) {
|
||||
const file = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
|
||||
fs.writeFileSync(file, JSON.stringify(tableData, null, 2))
|
||||
const file = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
|
||||
fs.writeFileSync(file, JSON.stringify(tableData, null, 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies audit items from the Stored Program's session directory (its
|
||||
* current working directory at runtime) into the staging subfolder:
|
||||
* - log.log -> submit.log (runtime log of the submission)
|
||||
* - code.js -> submit_code.js (program source as executed)
|
||||
* - reqHeaders.txt -> submit_reqHeaders.txt
|
||||
*/
|
||||
function copySessionAuditItems(stageFolder, prefix) {
|
||||
try {
|
||||
const sessionDir = nodePath.dirname(weboutPath)
|
||||
const copies = [
|
||||
['log.log', prefix + '.log'],
|
||||
['code.js', prefix + '_code.js'],
|
||||
['reqHeaders.txt', prefix + '_reqHeaders.txt']
|
||||
]
|
||||
for (const [src, dest] of copies) {
|
||||
const srcPath = nodePath.resolve(sessionDir, src)
|
||||
if (fs.existsSync(srcPath)) {
|
||||
fs.copyFileSync(srcPath, nodePath.resolve(stageFolder, dest))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error copying session audit items: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Parse input ──────────────────────────────────────────────────────────────
|
||||
@@ -32,74 +61,123 @@ const jsDataRaw = fetchRaw('jsdata')
|
||||
let jsDataHeaders = []
|
||||
let jsDataRows = []
|
||||
if (jsDataRaw) {
|
||||
try {
|
||||
const lines = jsDataRaw.split(/\r?\n/)
|
||||
if (lines.length >= 1) {
|
||||
jsDataHeaders = lines[0].trim().split(/\s+/).map(h => h.split(':')[0])
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) continue
|
||||
const values = []
|
||||
let cur = '', inQuote = false
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const ch = line[j]
|
||||
if (ch === '"') { if (inQuote && line[j+1] === '"') { cur += '"'; j++ } else inQuote = !inQuote }
|
||||
else if (ch === ',' && !inQuote) { values.push(cur); cur = '' }
|
||||
else cur += ch
|
||||
}
|
||||
values.push(cur)
|
||||
const row = {}
|
||||
for (let k = 0; k < jsDataHeaders.length && k < values.length; k++) row[jsDataHeaders[k]] = values[k]
|
||||
jsDataRows.push(row)
|
||||
}
|
||||
try {
|
||||
const lines = jsDataRaw.split(/\r?\n/)
|
||||
if (lines.length >= 1) {
|
||||
jsDataHeaders = lines[0]
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((h) => h.split(':')[0])
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) continue
|
||||
const values = []
|
||||
let cur = '',
|
||||
inQuote = false
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const ch = line[j]
|
||||
if (ch === '"') {
|
||||
if (inQuote && line[j + 1] === '"') {
|
||||
cur += '"'
|
||||
j++
|
||||
} else inQuote = !inQuote
|
||||
} else if (ch === ',' && !inQuote) {
|
||||
values.push(cur)
|
||||
cur = ''
|
||||
} else cur += ch
|
||||
}
|
||||
} catch(err) { console.log('Error parsing jsdata: ' + err.message) }
|
||||
values.push(cur)
|
||||
const row = {}
|
||||
for (let k = 0; k < jsDataHeaders.length && k < values.length; k++)
|
||||
row[jsDataHeaders[k]] = values[k]
|
||||
jsDataRows.push(row)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error parsing jsdata: ' + err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert formatted date/datetime/time strings back to SAS numeric values
|
||||
const schema = loadTableSchema(dataDir, dsn)
|
||||
if (schema.length > 0) {
|
||||
const colDdtypes = {}
|
||||
for (const col of schema) colDdtypes[col.name.toUpperCase()] = getDdType(col)
|
||||
jsDataRows = jsDataRows.map(row => {
|
||||
const converted = {}
|
||||
for (const key of Object.keys(row)) {
|
||||
const ddtype = colDdtypes[key.toUpperCase()]
|
||||
converted[key] = ddtype ? parseFormattedToSas(row[key], ddtype) : row[key]
|
||||
}
|
||||
return converted
|
||||
})
|
||||
const colDdtypes = {}
|
||||
for (const col of schema) colDdtypes[col.name.toUpperCase()] = getDdType(col)
|
||||
jsDataRows = jsDataRows.map((row) => {
|
||||
const converted = {}
|
||||
for (const key of Object.keys(row)) {
|
||||
const ddtype = colDdtypes[key.toUpperCase()]
|
||||
converted[key] = ddtype ? parseFormattedToSas(row[key], ddtype) : row[key]
|
||||
}
|
||||
return converted
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Generate table_id and 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,
|
||||
// temp diffs etc) for the audit trail.
|
||||
|
||||
const now = new Date()
|
||||
const pad = (n, w) => String(n).padStart(w, '0')
|
||||
const dtStr = now.getFullYear() + pad(now.getMonth()+1,2) + pad(now.getDate(),2) + '-' +
|
||||
pad(now.getHours(),2) + pad(now.getMinutes(),2) + pad(now.getSeconds(),2) + '.' +
|
||||
pad(now.getMilliseconds(),3)
|
||||
const dtStr =
|
||||
now.getFullYear() +
|
||||
pad(now.getMonth() + 1, 2) +
|
||||
pad(now.getDate(), 2) +
|
||||
'-' +
|
||||
pad(now.getHours(), 2) +
|
||||
pad(now.getMinutes(), 2) +
|
||||
pad(now.getSeconds(), 2) +
|
||||
'.' +
|
||||
pad(now.getMilliseconds(), 3)
|
||||
const rand6 = pad(Math.floor(Math.random() * 1000000), 6)
|
||||
const pid4 = pad(process.pid % 10000, 4)
|
||||
const tableId = `DC${dtStr}_${rand6}_${pid4}`
|
||||
|
||||
const stageFile = nodePath.resolve(stagingDir, tableId + '.json')
|
||||
const stageFolder = nodePath.resolve(stagingDir, tableId)
|
||||
fs.mkdirSync(stageFolder, { recursive: true })
|
||||
|
||||
const stageData = {
|
||||
table_id: tableId, libds, libref, dsn,
|
||||
headers: jsDataHeaders, rows: jsDataRows
|
||||
table_id: tableId,
|
||||
libds,
|
||||
libref,
|
||||
dsn,
|
||||
submitted_by_nm:
|
||||
typeof _METAUSER !== 'undefined' && _METAUSER ? _METAUSER : 'sasdemo',
|
||||
submitted_on_dttm: now.getTime() / 1000,
|
||||
headers: jsDataHeaders,
|
||||
rows: jsDataRows
|
||||
}
|
||||
fs.writeFileSync(stageFile, JSON.stringify(stageData, null, 2))
|
||||
fs.writeFileSync(
|
||||
nodePath.resolve(stageFolder, 'staged.json'),
|
||||
JSON.stringify(stageData, null, 2)
|
||||
)
|
||||
|
||||
// keep audit items from this Stored Program run alongside the staged data
|
||||
copySessionAuditItems(stageFolder, 'submit')
|
||||
|
||||
// Insert a record into MPE_SUBMIT
|
||||
const submitTable = loadTableData('MPE_SUBMIT') || { name: 'MPE_SUBMIT', columns: [], rows: [] }
|
||||
const submitTable = loadTableData('MPE_SUBMIT') || {
|
||||
name: 'MPE_SUBMIT',
|
||||
columns: [],
|
||||
rows: []
|
||||
}
|
||||
submitTable.rows.push({
|
||||
TABLE_ID: tableId, SUBMIT_STATUS_CD: 'SUBMITTED',
|
||||
BASE_LIB: libref, BASE_DS: dsn,
|
||||
SUBMITTED_BY_NM: 'sasdemo', SUBMITTED_ON_DTTM: now.getTime() / 1000,
|
||||
SUBMITTED_REASON_TXT: message,
|
||||
INPUT_OBS: jsDataRows.length,
|
||||
INPUT_VARS: jsDataHeaders.filter(h => h !== '_____DELETE__THIS__RECORD_____').length,
|
||||
NUM_OF_APPROVALS_REQUIRED: 1, NUM_OF_APPROVALS_REMAINING: 1,
|
||||
REVIEWED_BY_NM: '', REVIEWED_ON_DTTM: null
|
||||
TABLE_ID: tableId,
|
||||
SUBMIT_STATUS_CD: 'SUBMITTED',
|
||||
BASE_LIB: libref,
|
||||
BASE_DS: dsn,
|
||||
SUBMITTED_BY_NM: stageData.submitted_by_nm,
|
||||
SUBMITTED_ON_DTTM: stageData.submitted_on_dttm,
|
||||
SUBMITTED_REASON_TXT: message,
|
||||
INPUT_OBS: jsDataRows.length,
|
||||
INPUT_VARS: jsDataHeaders.filter(
|
||||
(h) => h !== '_____DELETE__THIS__RECORD_____'
|
||||
).length,
|
||||
NUM_OF_APPROVALS_REQUIRED: 1,
|
||||
NUM_OF_APPROVALS_REMAINING: 1,
|
||||
REVIEWED_BY_NM: '',
|
||||
REVIEWED_ON_DTTM: null
|
||||
})
|
||||
saveTableData('MPE_SUBMIT', submitTable)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user