Two findings on #506. createdemodata eval'd the compiled settings program unconditionally, so a drive that has the fixtures but not that file went down at load with an ENOENT before it could report anything. The read is guarded: an absent program evals a stub that answers the same default dcReadSettings gives for a missing file, and the service settles on the control library makedata creates instead of failing. A settings program that exists and does not parse is still a broken deployment, and is left to fail loudly. viewIsoDateTime and viewIsoTime clamped a rounded fraction at scale-1 instead of carrying it, so a value 1e-7 under the next second printed .999999 of the second before it where SAS prints the next second, .000000. Both now round to the printed precision first and split the result, which carries into the second. Both are pinned: createdemodata seeds with settings.js deleted, and an E8601DT26.6 value one rounding error short of the next minute prints 03:05:00.
1571 lines
41 KiB
JavaScript
1571 lines
41 KiB
JavaScript
/**
|
|
* Demo / test fixtures for the JS-only (SASjs Server) estate.
|
|
*
|
|
* makedata builds what `mpe_makedatamodel` + `mpe_makedata` build: the control
|
|
* library only. The Cypress E2E suite, though, needs tables that are not part
|
|
* of a Data Controller install - the TESTDATA demo library, the deep and wide
|
|
* search fixtures, the multi-load library. Those used to be seeded by
|
|
* makedata itself, which meant the mock estate never matched the estate the
|
|
* SAS service builds. They live here instead, and the test estate runs this
|
|
* service after makedata.
|
|
*
|
|
* Nothing here is deployed to a real site: a SASjs Server deployment of DC runs
|
|
* makedata, not this.
|
|
*
|
|
* Run against the mock estate (after makedata) with:
|
|
*
|
|
* sasjs request services/admin/createdemodata -t server-ci
|
|
*
|
|
* It is safe to run more than once: the fixture rows are stripped from the
|
|
* control tables before they are added back, so a reseed does not accumulate.
|
|
*/
|
|
const nodePath = require('path')
|
|
|
|
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
|
|
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
|
|
const appLoc = nodePath.join(..._program.split('services')[0].split('/'))
|
|
|
|
const settingsProgram = nodePath.resolve(
|
|
driveRoot,
|
|
'files',
|
|
appLoc,
|
|
'services',
|
|
'dcSettings.js'
|
|
)
|
|
/*
|
|
A drive makedata has not seeded has no compiled settings program. Eval a stub
|
|
with the same answer rather than throwing at load: the library makedata creates
|
|
is the fallback, and it is what dcReadSettings itself answers for a missing
|
|
file, but this service cannot call it if the file that defines it is absent. A
|
|
settings program that exists and does not parse is a broken deployment and is
|
|
left to fail loudly.
|
|
*/
|
|
const settingsSource = fs.existsSync(settingsProgram)
|
|
? fs.readFileSync(settingsProgram, 'utf8')
|
|
: 'function dcReadSettings() { return { dcLibref: "DC_JSLIB" } }'
|
|
eval(settingsSource)
|
|
const dcLibref = dcReadSettings(driveRoot, appLoc).dcLibref
|
|
const dataDir = nodePath.resolve(driveRoot, 'files', appLoc, 'data', dcLibref)
|
|
|
|
let adminGroup = 'AllUsers'
|
|
if (typeof admin !== 'undefined') adminGroup = admin
|
|
|
|
/* The demo libraries these fixtures create. */
|
|
const testDataLibref = 'TESTDATA'
|
|
const multiLoadLibref = 'DC996664'
|
|
|
|
/* The fixture libraries and tables this service owns. */
|
|
const fixtureLibrefs = ['TESTDATA', 'DC996664']
|
|
const fixtureTables = ['MPE_X_SEARCH', 'MPE_X_WIDE']
|
|
|
|
function tableFile(tableName) {
|
|
return nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
|
|
}
|
|
|
|
function readTable(tableName) {
|
|
const file = tableFile(tableName)
|
|
if (!fs.existsSync(file)) return null
|
|
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
}
|
|
|
|
function readRows(tableName) {
|
|
const table = readTable(tableName)
|
|
if (!table) {
|
|
throw new Error(
|
|
'createdemodata needs the control library - run services/admin/makedata first (' +
|
|
tableName +
|
|
' is missing)'
|
|
)
|
|
}
|
|
return table.rows || []
|
|
}
|
|
|
|
/* Column metadata is makedata's; a fixture table of its own supplies its own. */
|
|
const schema = {}
|
|
|
|
function saveTable(tableName, rows, columns) {
|
|
const existing = readTable(tableName)
|
|
const cols = columns || (existing && existing.columns) || []
|
|
/* A rerun re-adds every fixture row; drop exact duplicates so the estate
|
|
does not grow a second copy each time. */
|
|
const seen = {}
|
|
const unique = []
|
|
for (const row of rows) {
|
|
const key = JSON.stringify(row, Object.keys(row).sort())
|
|
if (seen[key]) continue
|
|
seen[key] = true
|
|
unique.push(row)
|
|
}
|
|
fs.writeFileSync(
|
|
tableFile(tableName),
|
|
JSON.stringify({ name: tableName, columns: cols, rows: unique }, null, 2)
|
|
)
|
|
}
|
|
|
|
/* The control tables the fixtures add rows to. */
|
|
const tables = {}
|
|
for (const name of [
|
|
'MPE_TABLES',
|
|
'MPE_VALIDATIONS',
|
|
'MPE_SELECTBOX',
|
|
'MPE_SECURITY',
|
|
'MPE_DERIVED'
|
|
]) {
|
|
tables[name] = readRows(name)
|
|
}
|
|
|
|
/* Drop fixture rows from a previous run before adding them back. */
|
|
tables.MPE_TABLES = tables.MPE_TABLES.filter(function (row) {
|
|
const libref = String(row.libref || '').toUpperCase()
|
|
const dsn = String(row.dsn || '').toUpperCase()
|
|
return (
|
|
fixtureLibrefs.indexOf(libref) < 0 && fixtureTables.indexOf(dsn) < 0
|
|
)
|
|
})
|
|
tables.MPE_VALIDATIONS = tables.MPE_VALIDATIONS.filter(function (row) {
|
|
const lib = String(row.base_lib || row.BASE_LIB || '').toUpperCase()
|
|
const dsn = String(row.base_ds || row.BASE_DS || '').toUpperCase()
|
|
if (lib === testDataLibref) return false
|
|
/*
|
|
makedata carries SAS's demo rules for MPE_X_TEST, one of which the excel
|
|
fixtures cannot satisfy: the HARDREGEX on SOME_SHORTNUM rejects 1-5, and
|
|
regular_excel.xlsx - the file the specs upload to dc_jslib - has five rows
|
|
where SOME_SHORTNUM is 5. The upload is refused before it reaches the
|
|
approval screen, so the specs never see an Approve button. SAS's other rules
|
|
for the table are the ones the mock has always had and the editor specs
|
|
exercise (HARDSELECT_HOOK on SOME_NUM among them), so only this one goes.
|
|
*/
|
|
if (dsn === 'MPE_X_TEST') {
|
|
return !(
|
|
String(row.base_col || row.BASE_COL || '').toUpperCase() ===
|
|
'SOME_SHORTNUM' &&
|
|
String(row.rule_type || row.RULE_TYPE || '').toUpperCase() === 'HARDREGEX'
|
|
)
|
|
}
|
|
return true
|
|
})
|
|
tables.MPE_SELECTBOX = tables.MPE_SELECTBOX.filter(function (row) {
|
|
return String(row.select_lib || row.SELECT_LIB || '').toUpperCase() !== 'TESTDATA'
|
|
})
|
|
|
|
/*
|
|
MPE_DERIVED is a control table makedata writes (empty on a stock install),
|
|
so the fixture rows are filtered out and appended in the same shape as the
|
|
tables above - assigning the array would wipe any row makedata owns.
|
|
*/
|
|
const testDerived = [
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 253717919999,
|
|
libref: testDataLibref,
|
|
dsn: 'DERIVED_DEMO',
|
|
var_nm: 'REGION',
|
|
var_deps: 'STATE',
|
|
var_type: 'C',
|
|
var_len: 8,
|
|
var_fmt: '',
|
|
var_logic: "ifc(STATE in ('CA','OR','WA'),'West','East')",
|
|
active: 1
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 253717919999,
|
|
libref: testDataLibref,
|
|
dsn: 'DERIVED_DEMO',
|
|
var_nm: 'LINE_VALUE',
|
|
var_deps: 'QTY PRICE',
|
|
var_type: 'N',
|
|
var_len: 8,
|
|
var_fmt: 'dollar12.2',
|
|
var_logic: 'QTY * PRICE',
|
|
active: 1
|
|
},
|
|
{
|
|
// built on REGION, so it proves the dependency order is applied
|
|
tx_from: 0,
|
|
tx_to: 253717919999,
|
|
libref: testDataLibref,
|
|
dsn: 'DERIVED_DEMO',
|
|
var_nm: 'REGION_LABEL',
|
|
var_deps: 'REGION STATE',
|
|
var_type: 'C',
|
|
var_len: 14,
|
|
var_fmt: '',
|
|
var_label: 'Region and state',
|
|
var_logic: "cats(REGION,'-',STATE)",
|
|
active: 1
|
|
}
|
|
]
|
|
|
|
tables.MPE_DERIVED = tables.MPE_DERIVED.filter(function (row) {
|
|
return String(row.libref || row.LIBREF || '').toUpperCase() !== 'TESTDATA'
|
|
})
|
|
for (const row of testDerived) tables.MPE_DERIVED.push(row)
|
|
|
|
// ─── MPE_X_SEARCH: viewer full-table-search demo table ───────────────────────
|
|
// Deterministic, obviously fictional deep-sea survey data (no real sites,
|
|
// vessels or people). Laid out site-major / vessel-minor so every count in
|
|
// the full-table-search Cypress spec can be derived by hand:
|
|
// n = rep * 50 + siteIdx * 5 + vesselIdx (0-based, 1000 rows)
|
|
// -> 20 rows per site, 100 rows per site block, 200 rows per vessel
|
|
const searchSites = [
|
|
'Nereid Trench',
|
|
'Calypso Trench',
|
|
'Halcyon Trench',
|
|
'Amphitrite Basin',
|
|
'Kraken Hollow',
|
|
'Erebus Deep',
|
|
'Thalassa Shelf',
|
|
'Oceanus Rise',
|
|
'Selkie Bank',
|
|
'Vostok Deep'
|
|
]
|
|
const searchVessels = [
|
|
'RV Pelagia',
|
|
'RV Aurora',
|
|
'RV Selkie',
|
|
'RV Cormorant',
|
|
'RV Halcyon'
|
|
]
|
|
const searchSpecies = [
|
|
'Vampire squid',
|
|
'Barreleye fish',
|
|
'Dumbo octopus',
|
|
'Yeti crab',
|
|
'Giant isopod',
|
|
'Ghost shark',
|
|
'Comb jelly',
|
|
'Sea pig',
|
|
'Glass sponge',
|
|
'Anglerfish'
|
|
]
|
|
// Rows carrying the mid-field marker in NOTES (searched as a partial string)
|
|
const siphonophoreRows = [137, 500, 862]
|
|
// Rows sharing the repeated exact SAMPLE_COUNT (searched as a number)
|
|
const sampleCountRows = [220, 780]
|
|
|
|
tables.MPE_X_SEARCH = []
|
|
for (let n = 0; n < 1000; n++) {
|
|
const rep = Math.floor(n / 50)
|
|
const siteIdx = Math.floor((n % 50) / 5)
|
|
const vesselIdx = n % 5
|
|
const site = searchSites[siteIdx]
|
|
const depth = 1000 + (n % 90) * 37
|
|
tables.MPE_X_SEARCH.push({
|
|
PRIMARY_KEY_FIELD: n + 1001,
|
|
SITE_NAME: site,
|
|
VESSEL: searchVessels[vesselIdx],
|
|
SAMPLE_COUNT: sampleCountRows.includes(n) ? 4210 : 1 + (n % 6),
|
|
DEPTH_M: depth,
|
|
SPECIES: searchSpecies[(siteIdx + vesselIdx) % searchSpecies.length],
|
|
CRUISE_DATE: 20000 + (n % 3650),
|
|
EXPEDITION_ID: 'EXP-' + String(n + 1).padStart(4, '0'),
|
|
NOTES:
|
|
'Leg ' +
|
|
(rep + 1) +
|
|
' of the ' +
|
|
site +
|
|
' survey. ' +
|
|
(siphonophoreRows.includes(n)
|
|
? 'A bioluminescent siphonophore was filmed here.'
|
|
: 'No anomalies recorded.') +
|
|
' Depth soundings logged at ' +
|
|
depth +
|
|
'm.'
|
|
})
|
|
}
|
|
|
|
// Register the demo table like any other editable table (buskey drives the
|
|
// viewer's PK detection).
|
|
tables.MPE_TABLES.push({
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: dcLibref,
|
|
dsn: 'MPE_X_SEARCH',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'PRIMARY_KEY_FIELD',
|
|
notes: 'Full table search demo table'
|
|
})
|
|
|
|
// ─── MPE_X_WIDE: wide table for the EDIT cell limit ─────────────────────────
|
|
// 200 rows x 1001 columns (ROW_ID + 500 single-character + 500 numeric) is
|
|
// 200,200 cells - over DC_MAXCELLS_WEBEDIT (200000) while staying under
|
|
// DC_MAXOBS_WEBEDIT (250), so it exercises the CELL limit on its own and the
|
|
// row limit spec can use MPE_X_SEARCH (1000 rows) instead. Values are one
|
|
// character / one digit on purpose: the point is width, and small values keep
|
|
// the seeded JSON near 1MB rather than the 25MB the real WIDEBOY fixture (20
|
|
// character fields) would take.
|
|
tables.MPE_X_WIDE = []
|
|
for (let n = 0; n < 200; n++) {
|
|
const wideRow = { ROW_ID: n + 1 }
|
|
for (let c = 1; c <= 500; c++) {
|
|
wideRow['CHAR' + c] = String.fromCharCode(65 + ((n + c) % 26))
|
|
}
|
|
for (let c = 1; c <= 500; c++) {
|
|
wideRow['NUM' + c] = (n + c) % 10
|
|
}
|
|
tables.MPE_X_WIDE.push(wideRow)
|
|
}
|
|
|
|
tables.MPE_TABLES.push({
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: dcLibref,
|
|
dsn: 'MPE_X_WIDE',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'ROW_ID CHAR1 NUM1',
|
|
notes: 'Wide table fixture for the EDIT cell limit'
|
|
})
|
|
|
|
// Column metadata for the fixture tables. makedata carries the control tables'
|
|
// schema; these two are fixtures, so their columns travel with the fixtures -
|
|
// saveTable falls back to the file already on the drive, and a fresh estate
|
|
// (CI deletes the appLoc first) has no file to fall back to.
|
|
schema.MPE_X_SEARCH = [
|
|
{
|
|
name: 'PRIMARY_KEY_FIELD',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Primary Key',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'SITE_NAME',
|
|
type: 'C',
|
|
length: 32,
|
|
format: '',
|
|
label: 'Survey Site',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'VESSEL',
|
|
type: 'C',
|
|
length: 32,
|
|
format: '',
|
|
label: 'Survey Vessel',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SAMPLE_COUNT',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Samples Collected',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'DEPTH_M',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Depth (metres)',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SPECIES',
|
|
type: 'C',
|
|
length: 32,
|
|
format: '',
|
|
label: 'Species Observed',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'CRUISE_DATE',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'date9.',
|
|
label: 'Cruise Date',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'EXPEDITION_ID',
|
|
type: 'C',
|
|
length: 12,
|
|
format: '',
|
|
label: 'Expedition',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'NOTES',
|
|
type: 'C',
|
|
length: 512,
|
|
format: '',
|
|
label: 'Survey Notes',
|
|
notnull: false
|
|
}
|
|
]
|
|
|
|
// Columns for MPE_X_WIDE - built rather than spelled out, there are 1001.
|
|
schema.MPE_X_WIDE = [
|
|
{ name: 'ROW_ID', type: 'N', length: 8, format: '', label: '', notnull: true }
|
|
]
|
|
for (let c = 1; c <= 500; c++) {
|
|
schema.MPE_X_WIDE.push({
|
|
name: 'CHAR' + c,
|
|
type: 'C',
|
|
length: 1,
|
|
format: '',
|
|
label: '',
|
|
notnull: false
|
|
})
|
|
}
|
|
for (let c = 1; c <= 500; c++) {
|
|
schema.MPE_X_WIDE.push({
|
|
name: 'NUM' + c,
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: '',
|
|
notnull: false
|
|
})
|
|
}
|
|
|
|
// ─── TESTDATA library: demo user tables for the Cypress E2E suite ───────────
|
|
// These tables hold the DQ-rule demo columns (HARDSELECT, READONLY, HIDDEN,
|
|
// ROUND, NUMBER_FORMAT, HARDREGEX, SOFTREGEX) and the formula demo columns
|
|
// (HARDFORMULA, SOFTFORMULA, DC.* references) that the editor specs assert
|
|
// against. They live in a separate library so DC_JSLIB.MPE_X_TEST keeps
|
|
// matching the excel upload fixtures (which predate these columns), and so
|
|
// the editor/openTableFromTree flow exercises a real two-library nav tree
|
|
// (DC_JSLIB + TESTDATA), the same way a real SAS site is laid out.
|
|
const testDataDir = nodePath.resolve(
|
|
driveRoot,
|
|
'files',
|
|
appLoc,
|
|
'data',
|
|
testDataLibref
|
|
)
|
|
if (!fs.existsSync(testDataDir)) {
|
|
fs.mkdirSync(testDataDir, { recursive: true })
|
|
}
|
|
|
|
const testTables = {
|
|
MPE_X_NEW: [
|
|
{
|
|
PRIMARY_KEY_FIELD: 0,
|
|
SOME_CHAR: 'this is dummy data',
|
|
SOME_DROPDOWN: 'Option 1',
|
|
SOME_HARDSELECT: 'Alpha',
|
|
SOME_NUM: 0.00105564761956,
|
|
SOME_DATE: 42,
|
|
SOME_DATETIME: 42,
|
|
SOME_TIME: 42,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 1.142857,
|
|
NUMFMT_COL: 1000,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
PRIMARY_KEY_FIELD: 1,
|
|
SOME_CHAR: 'more dummy data',
|
|
SOME_DROPDOWN: 'Option 2',
|
|
SOME_HARDSELECT: 'Bravo',
|
|
SOME_NUM: 0.00521895988156,
|
|
SOME_DATE: 42,
|
|
SOME_DATETIME: 42,
|
|
SOME_TIME: 422,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 2.285714,
|
|
NUMFMT_COL: 1012.5,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
PRIMARY_KEY_FIELD: 2,
|
|
SOME_CHAR: 'even more dummy data',
|
|
SOME_DROPDOWN: 'Option 3',
|
|
SOME_HARDSELECT: 'Charlie',
|
|
SOME_NUM: 0.0058409725343,
|
|
SOME_DATE: 42,
|
|
SOME_DATETIME: 42,
|
|
SOME_TIME: 142,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 3.428571,
|
|
NUMFMT_COL: 1025,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
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:',
|
|
SOME_DROPDOWN: 'Option 2',
|
|
SOME_HARDSELECT: 'Alpha',
|
|
SOME_NUM: 0.00613050395908,
|
|
SOME_DATE: 423,
|
|
SOME_DATETIME: 423,
|
|
SOME_TIME: 44,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 4.571428,
|
|
NUMFMT_COL: 1037.5,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
PRIMARY_KEY_FIELD: 4,
|
|
SOME_CHAR: 'if you can fill the unforgiving minute',
|
|
SOME_DROPDOWN: 'Option 1',
|
|
SOME_HARDSELECT: 'Bravo',
|
|
SOME_NUM: 0.01305025071513,
|
|
SOME_DATE: 4231,
|
|
SOME_DATETIME: 423123123,
|
|
SOME_TIME: 412,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 5.714285,
|
|
NUMFMT_COL: 1050,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
PRIMARY_KEY_FIELD: 5,
|
|
SOME_CHAR: '= testing the formula situation',
|
|
SOME_DROPDOWN: 'Option 1',
|
|
SOME_HARDSELECT: 'Charlie',
|
|
SOME_NUM: 0.01442142483518,
|
|
SOME_DATE: 42,
|
|
SOME_DATETIME: 42,
|
|
SOME_TIME: 42,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 6.857142,
|
|
NUMFMT_COL: 1062.5,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
},
|
|
{
|
|
PRIMARY_KEY_FIELD: 6,
|
|
SOME_CHAR: "'=the formula with leading apostrophe",
|
|
SOME_DROPDOWN: 'Option 1',
|
|
SOME_HARDSELECT: 'Alpha',
|
|
SOME_NUM: 0.02422838845487,
|
|
SOME_DATE: 42,
|
|
SOME_DATETIME: 42,
|
|
SOME_TIME: 42,
|
|
SOME_SHORTNUM: 8,
|
|
SOME_BESTNUM: 44,
|
|
READONLY_COL: 'Readonly default',
|
|
HIDDEN_COL: 'Hidden default',
|
|
ROUND_COL: 8,
|
|
NUMFMT_COL: 1075,
|
|
REGEX_HARD_COL: 'user@example.com',
|
|
REGEX_SOFT_COL: 'SW1A 1AA',
|
|
REGEX_BOTH_COL: 'ABC-123'
|
|
}
|
|
],
|
|
MPE_X_FORMULA_TEST: Array.from({ length: 10 }, (_, i) => ({
|
|
_____DELETE__THIS__RECORD_____: 'No',
|
|
PRIMARY_KEY_FIELD: i + 1,
|
|
A_COL: i + 1,
|
|
B_COL: 10,
|
|
FORMULA_HARD_COL:
|
|
i === 6 ? '7771' : i === 8 ? '9991' : i === 9 ? '1111' : '',
|
|
FORMULA_SOFT_COL:
|
|
i === 7 ? '8882' : i === 8 ? '9992' : i === 9 ? '2222' : '',
|
|
ROW_STATUS_COL: '',
|
|
USER_NAME_COL: '',
|
|
ORIG_VALUE_COL: 'orig-' + (i + 1),
|
|
CHANGE_SUMMARY_COL: 'orig-' + (i + 1),
|
|
PLAIN_TEXT_COL:
|
|
i === 4
|
|
? '=100 + B_COL'
|
|
: i === 5
|
|
? '=100 + 200'
|
|
: i === 6
|
|
? "'=genuine literal"
|
|
: 'note-' + (i + 1)
|
|
})),
|
|
MPE_X_FORMULA_PK_TEST: [
|
|
{
|
|
_____DELETE__THIS__RECORD_____: 'No',
|
|
PK_HARDFORMULA_COL: '',
|
|
PK_SOFTFORMULA_COL: '',
|
|
A_COL: 2,
|
|
B_COL: 3
|
|
},
|
|
{
|
|
_____DELETE__THIS__RECORD_____: 'No',
|
|
PK_HARDFORMULA_COL: '',
|
|
PK_SOFTFORMULA_COL: '',
|
|
A_COL: 5,
|
|
B_COL: 4
|
|
}
|
|
],
|
|
// DEMO_01 - compact rule-bearing table, eight columns so it fits a narrow
|
|
// viewport without horizontal scrolling. Used for demos and clips that need
|
|
// the rule behaviours visible on one screen.
|
|
DEMO_01: [
|
|
{
|
|
ID: 1,
|
|
AMOUNT: 120,
|
|
SCORE: 82,
|
|
GRADE: '.a',
|
|
REF: 1001,
|
|
RATING: 4,
|
|
STATUS: 1,
|
|
LAST_REVIEWED: 23390
|
|
},
|
|
// One row already carries a special missing, so a demo can show a change
|
|
// from one special missing to another (the DIFF compares them).
|
|
{
|
|
ID: 2,
|
|
AMOUNT: 95,
|
|
SCORE: 91,
|
|
GRADE: '.b',
|
|
REF: 1002,
|
|
RATING: '.a',
|
|
STATUS: 2,
|
|
LAST_REVIEWED: 23435
|
|
},
|
|
// STATUS carries the special missing that its own SOFTSELECT dropdown
|
|
// lists, which is what makes the dropdown worth demonstrating.
|
|
{
|
|
ID: 3,
|
|
AMOUNT: 430,
|
|
SCORE: 76,
|
|
GRADE: '.c',
|
|
REF: 1003,
|
|
RATING: 3,
|
|
STATUS: '.a',
|
|
LAST_REVIEWED: 23466
|
|
},
|
|
{
|
|
ID: 4,
|
|
AMOUNT: 88,
|
|
SCORE: 88,
|
|
GRADE: '.a',
|
|
REF: 1004,
|
|
RATING: 4,
|
|
STATUS: 3,
|
|
LAST_REVIEWED: 23502
|
|
},
|
|
{
|
|
ID: 5,
|
|
AMOUNT: 210,
|
|
SCORE: 95,
|
|
GRADE: '.b',
|
|
REF: 1005,
|
|
RATING: 5,
|
|
STATUS: 1,
|
|
LAST_REVIEWED: 23557
|
|
},
|
|
{
|
|
ID: 6,
|
|
AMOUNT: 65,
|
|
SCORE: 64,
|
|
GRADE: '.c',
|
|
REF: 1006,
|
|
RATING: 2,
|
|
STATUS: 3,
|
|
LAST_REVIEWED: 23561
|
|
},
|
|
{
|
|
ID: 7,
|
|
AMOUNT: 150,
|
|
SCORE: 73,
|
|
GRADE: '.a',
|
|
REF: 1007,
|
|
RATING: 3,
|
|
STATUS: 2,
|
|
LAST_REVIEWED: 23608
|
|
},
|
|
{
|
|
ID: 8,
|
|
AMOUNT: 300,
|
|
SCORE: 99,
|
|
GRADE: '.b',
|
|
REF: 1008,
|
|
RATING: 5,
|
|
STATUS: 1,
|
|
LAST_REVIEWED: 23621
|
|
}
|
|
],
|
|
// Hook demo pair: DEMO_MIRROR is an EMPTY mirror of DEMO_ORDERS, registered
|
|
// with pre/post edit hooks (services/hooks/demo_mirror_*.js). The pre-edit
|
|
// hook shows the live rows of DEMO_ORDERS; the post-edit hook routes a
|
|
// changeset submitted against the mirror to DEMO_ORDERS.
|
|
DEMO_ORDERS: [
|
|
{ ORDER_ID: 1001, CUSTOMER: 'Acme Corp', REGION: 'EU', AMOUNT: 450, STATUS: 'NEW' },
|
|
{ ORDER_ID: 1002, CUSTOMER: 'Borealis Ltd', REGION: 'EU', AMOUNT: 980.5, STATUS: 'OPEN' },
|
|
{ ORDER_ID: 1003, CUSTOMER: 'Cobalt GmbH', REGION: 'EU', AMOUNT: 620, STATUS: 'CLOSED' },
|
|
{ ORDER_ID: 1004, CUSTOMER: 'Delta Systems', REGION: 'US', AMOUNT: 4500, STATUS: 'NEW' }
|
|
],
|
|
DEMO_MIRROR: [],
|
|
// Derived variable demo - REGION and LINE_VALUE are not columns of this
|
|
// table, they are computed at read time from MPE_DERIVED rows.
|
|
DERIVED_DEMO: [
|
|
{ ID: 1, STATE: 'CA', QTY: 2, PRICE: 10.5 },
|
|
{ ID: 2, STATE: 'NY', QTY: 1, PRICE: 99 },
|
|
{ ID: 3, STATE: 'OR', QTY: 5, PRICE: 3 },
|
|
{ ID: 4, STATE: 'TX', QTY: 3, PRICE: 7.25 }
|
|
]
|
|
}
|
|
|
|
// Schemas for the TESTDATA tables, matching the old inline getdata.js payload
|
|
// that the Cypress editor specs were calibrated against.
|
|
const testSchema = {
|
|
// Hook demo pair (see DEMO_ORDERS / DEMO_MIRROR in testTables). The mirror
|
|
// has the same shape as the real table, so the pre-edit hook can hand its rows
|
|
// straight to the grid.
|
|
DEMO_ORDERS: [
|
|
{ name: 'ORDER_ID', type: 'N', length: 8, format: '', label: 'Order Id', notnull: true },
|
|
{ name: 'CUSTOMER', type: 'C', length: 20, format: '', label: 'Customer', notnull: false },
|
|
{ name: 'REGION', type: 'C', length: 8, format: '', label: 'Region', notnull: false },
|
|
{ name: 'AMOUNT', type: 'N', length: 8, format: '', label: 'Amount', notnull: false },
|
|
{ name: 'STATUS', type: 'C', length: 12, format: '', label: 'Status', notnull: false }
|
|
],
|
|
DEMO_MIRROR: [
|
|
{ name: 'ORDER_ID', type: 'N', length: 8, format: '', label: 'Order Id', notnull: true },
|
|
{ name: 'CUSTOMER', type: 'C', length: 20, format: '', label: 'Customer', notnull: false },
|
|
{ name: 'REGION', type: 'C', length: 8, format: '', label: 'Region', notnull: false },
|
|
{ name: 'AMOUNT', type: 'N', length: 8, format: '', label: 'Amount', notnull: false },
|
|
{ name: 'STATUS', type: 'C', length: 12, format: '', label: 'Status', notnull: false }
|
|
],
|
|
MPE_X_NEW: [
|
|
{
|
|
name: 'PRIMARY_KEY_FIELD',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: '',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'SOME_CHAR',
|
|
type: 'C',
|
|
length: 32767,
|
|
format: '',
|
|
label: 'Some Character Column',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_DROPDOWN',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'SOME_DROPDOWN',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_HARDSELECT',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'SOME_HARDSELECT',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_NUM',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: '',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_DATE',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'date9.',
|
|
label: 'Some Date',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_DATETIME',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'datetime19.',
|
|
label: 'SOME_DATETIME',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_TIME',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'time8.',
|
|
label: 'SOME_TIME',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_SHORTNUM',
|
|
type: 'N',
|
|
length: 4,
|
|
format: '',
|
|
label: 'SOME_SHORTNUM',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SOME_BESTNUM',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'best.',
|
|
label: 'SOME_BESTNUM',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'READONLY_COL',
|
|
type: 'C',
|
|
length: 200,
|
|
format: '',
|
|
label: 'READONLY_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'HIDDEN_COL',
|
|
type: 'C',
|
|
length: 200,
|
|
format: '',
|
|
label: 'HIDDEN_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'ROUND_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'ROUND_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'NUMFMT_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'NUMFMT_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'REGEX_HARD_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'REGEX_HARD_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'REGEX_SOFT_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'REGEX_SOFT_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'REGEX_BOTH_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'REGEX_BOTH_COL',
|
|
notnull: false
|
|
}
|
|
],
|
|
MPE_X_FORMULA_TEST: [
|
|
{
|
|
name: 'PRIMARY_KEY_FIELD',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: '',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'A_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'A_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'B_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'B_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'FORMULA_HARD_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'FORMULA_HARD_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'FORMULA_SOFT_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'FORMULA_SOFT_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'ROW_STATUS_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'ROW_STATUS_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'USER_NAME_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'USER_NAME_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'ORIG_VALUE_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'ORIG_VALUE_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'CHANGE_SUMMARY_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'CHANGE_SUMMARY_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'PLAIN_TEXT_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'PLAIN_TEXT_COL',
|
|
notnull: false
|
|
}
|
|
],
|
|
MPE_X_FORMULA_PK_TEST: [
|
|
{
|
|
name: 'PK_HARDFORMULA_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'PK_HARDFORMULA_COL',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'PK_SOFTFORMULA_COL',
|
|
type: 'C',
|
|
length: 128,
|
|
format: '',
|
|
label: 'PK_SOFTFORMULA_COL',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'A_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'A_COL',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'B_COL',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'B_COL',
|
|
notnull: false
|
|
}
|
|
],
|
|
DEMO_01: [
|
|
{
|
|
name: 'ID',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'ID',
|
|
notnull: true
|
|
},
|
|
{
|
|
name: 'AMOUNT',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Amount',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'SCORE',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Score',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'GRADE',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Grade',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'REF',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Reference',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'RATING',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Rating',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'STATUS',
|
|
type: 'N',
|
|
length: 8,
|
|
format: '',
|
|
label: 'Status',
|
|
notnull: false
|
|
},
|
|
{
|
|
name: 'LAST_REVIEWED',
|
|
type: 'N',
|
|
length: 8,
|
|
format: 'date9.',
|
|
label: 'Last reviewed',
|
|
notnull: false
|
|
}
|
|
],
|
|
DERIVED_DEMO: [
|
|
{ name: 'ID', type: 'N', length: 8, format: '', label: 'ID', notnull: true },
|
|
{ name: 'STATE', type: 'C', length: 2, format: '', label: 'State', notnull: false },
|
|
{ name: 'QTY', type: 'N', length: 8, format: '', label: 'Quantity', notnull: false },
|
|
{ name: 'PRICE', type: 'N', length: 8, format: 'dollar12.2', label: 'Price', notnull: false }
|
|
]
|
|
}
|
|
|
|
// Register the TESTDATA tables in MPE_TABLES and add their DQ rules to
|
|
// MPE_VALIDATIONS, the same way mpe_makedata.sas configures a real site.
|
|
// The tests open these tables from the nav tree and edit/submit them, so
|
|
// they need MPE_TABLES registrations (buskey, loadtype) to be editable.
|
|
tables.MPE_TABLES.push(
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'MPE_X_NEW',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'PRIMARY_KEY_FIELD',
|
|
notes: 'DQ rule demo table'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'MPE_X_FORMULA_TEST',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'PRIMARY_KEY_FIELD',
|
|
notes: 'Formula demo table'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'MPE_X_FORMULA_PK_TEST',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'PK_HARDFORMULA_COL PK_SOFTFORMULA_COL',
|
|
notes: 'Formula PK demo table'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'DEMO_01',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'ID',
|
|
notes: 'Compact rule demo table (eight columns)'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'DERIVED_DEMO',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'ID',
|
|
notes: 'Derived variable demo table'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'DEMO_ORDERS',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'ORDER_ID',
|
|
notes: 'Hook demo - the real table the mirror points at'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: testDataLibref,
|
|
dsn: 'DEMO_MIRROR',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'ORDER_ID',
|
|
notes: 'Hook demo - an empty mirror whose hooks show and route DEMO_ORDERS',
|
|
pre_edit_hook: 'services/hooks/demo_mirror_preedit',
|
|
post_edit_hook: 'services/hooks/demo_mirror_postedit'
|
|
}
|
|
)
|
|
|
|
// Grant EDIT/APPROVE access on TESTDATA tables to the admin group, mirroring
|
|
// the AllUsers VIEW + dc-admin EDIT/APPROVE rows that MPE_SECURITY carries
|
|
// for DC_JSLIB tables.
|
|
tables.MPE_SECURITY.push(
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: '*ALL*',
|
|
dsn: '*ALL*',
|
|
access_level: 'EDIT',
|
|
sas_group: adminGroup
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: '*ALL*',
|
|
dsn: '*ALL*',
|
|
access_level: 'APPROVE',
|
|
sas_group: adminGroup
|
|
}
|
|
)
|
|
|
|
// DQ rules for the TESTDATA tables. These mirror the dqrules arrays the old
|
|
// inline getdata.js emitted, which the editor specs assert against.
|
|
const testValidations = [
|
|
// MPE_X_NEW
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'PRIMARY_KEY_FIELD',
|
|
rule_type: 'NOTNULL',
|
|
rule_value: ''
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'SOME_NUM',
|
|
rule_type: 'HARDSELECT_HOOK',
|
|
rule_value: 'services/validations/mpe_x_test.some_num'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'SOME_HARDSELECT',
|
|
rule_type: 'HARDSELECT',
|
|
rule_value: ''
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'READONLY_COL',
|
|
rule_type: 'READONLY',
|
|
rule_value: 'Readonly default'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'HIDDEN_COL',
|
|
rule_type: 'HIDDEN',
|
|
rule_value: 'Hidden default'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'ROUND_COL',
|
|
rule_type: 'ROUND',
|
|
rule_value: '2'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'NUMFMT_COL',
|
|
rule_type: 'NUMBER_FORMAT',
|
|
rule_value: '{"style":"currency","currency":"EUR"}'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'REGEX_HARD_COL',
|
|
rule_type: 'HARDREGEX',
|
|
rule_value: '/[\\w.]+@[\\w]+\\.[a-z]{2,}/'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'REGEX_SOFT_COL',
|
|
rule_type: 'SOFTREGEX',
|
|
rule_value: '/[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'REGEX_BOTH_COL',
|
|
rule_type: 'HARDREGEX',
|
|
rule_value: '/^[A-Z0-9_-]+$/'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_NEW',
|
|
base_col: 'REGEX_BOTH_COL',
|
|
rule_type: 'SOFTREGEX',
|
|
rule_value: '/^.{5,10}$/'
|
|
},
|
|
// MPE_X_FORMULA_TEST
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'PRIMARY_KEY_FIELD',
|
|
rule_type: 'NOTNULL',
|
|
rule_value: ''
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'FORMULA_HARD_COL',
|
|
rule_type: 'HARDFORMULA',
|
|
rule_value: '=A_COL * B_COL'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'FORMULA_SOFT_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value: '=A_COL + B_COL'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'ROW_STATUS_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value: '=DC.ROW_STATUS'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'USER_NAME_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value: '=DC.USER_NAME'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'ORIG_VALUE_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value: '=DC.ORIG_VALUE'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_TEST',
|
|
base_col: 'CHANGE_SUMMARY_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value:
|
|
'=IF( DC.ROW_STATUS ="U","unedited", DC.USER_NAME &" changed from "& DC.ORIG_VALUE )'
|
|
},
|
|
// MPE_X_FORMULA_PK_TEST
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_PK_TEST',
|
|
base_col: 'PK_HARDFORMULA_COL',
|
|
rule_type: 'HARDFORMULA',
|
|
rule_value: '=A_COL * B_COL'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'MPE_X_FORMULA_PK_TEST',
|
|
base_col: 'PK_SOFTFORMULA_COL',
|
|
rule_type: 'SOFTFORMULA',
|
|
rule_value: '="PK-" & A_COL'
|
|
},
|
|
// DEMO_01 - the compact rule demo table. A rule per column so each behaviour is
|
|
// visible in isolation: a numeric floor, a numeric ceiling, a floor and a
|
|
// ceiling expressed in special missings, a pattern on a numeric column, and a
|
|
// dropdown whose list is taken from the column itself (the library.member.column
|
|
// form the docs describe) so it can list the special missing that column holds.
|
|
// ID carries no rule: it is the table's buskey, and Data Controller applies NOT
|
|
// NULL to a primary key by itself.
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'AMOUNT',
|
|
rule_type: 'MINVAL',
|
|
rule_value: '1'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'SCORE',
|
|
rule_type: 'MAXVAL',
|
|
rule_value: '100'
|
|
},
|
|
// A range expressed in special missings, so it is the SAS order of the missing
|
|
// values that decides it: .A is the floor and .C the ceiling, which takes .B
|
|
// and refuses .D.
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'GRADE',
|
|
rule_type: 'MINVAL',
|
|
rule_value: '.A'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'GRADE',
|
|
rule_type: 'MAXVAL',
|
|
rule_value: '.C'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'REF',
|
|
rule_type: 'SOFTREGEX',
|
|
rule_value: '/^[0-9]+$/'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_01',
|
|
base_col: 'STATUS',
|
|
rule_type: 'SOFTSELECT',
|
|
rule_value: testDataLibref + '.DEMO_01.STATUS'
|
|
},
|
|
// Hook demo: the mirror carries its OWN rule set, separate from the real
|
|
// table's - which is the whole point of routing edits through a mirror.
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_MIRROR',
|
|
base_col: 'AMOUNT',
|
|
rule_type: 'MAXVAL',
|
|
rule_value: '2000'
|
|
},
|
|
{
|
|
base_lib: testDataLibref,
|
|
base_ds: 'DEMO_ORDERS',
|
|
base_col: 'AMOUNT',
|
|
rule_type: 'MAXVAL',
|
|
rule_value: '100000'
|
|
}
|
|
]
|
|
for (const v of testValidations) {
|
|
tables.MPE_VALIDATIONS.push({
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
...v,
|
|
rule_active: 1
|
|
})
|
|
}
|
|
|
|
// Selectbox values for the TESTDATA demo dropdowns (HARDSELECT/SOME_HARDSELECT)
|
|
const testSelectboxBase = {
|
|
select_lib: testDataLibref,
|
|
select_ds: 'MPE_X_NEW',
|
|
ver_from_dttm: 0,
|
|
ver_to_dttm: 127490111999
|
|
}
|
|
tables.MPE_SELECTBOX.push(
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1001,
|
|
base_column: 'SOME_HARDSELECT',
|
|
selectbox_value: 'Alpha',
|
|
selectbox_order: 1
|
|
},
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1002,
|
|
base_column: 'SOME_HARDSELECT',
|
|
selectbox_value: 'Bravo',
|
|
selectbox_order: 2
|
|
},
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1003,
|
|
base_column: 'SOME_HARDSELECT',
|
|
selectbox_value: 'Charlie',
|
|
selectbox_order: 3
|
|
},
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1004,
|
|
base_column: 'SOME_DROPDOWN',
|
|
selectbox_value: 'Option 1',
|
|
selectbox_order: 1
|
|
},
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1005,
|
|
base_column: 'SOME_DROPDOWN',
|
|
selectbox_value: 'Option 2',
|
|
selectbox_order: 2
|
|
},
|
|
{
|
|
...testSelectboxBase,
|
|
selectbox_rk: 1006,
|
|
base_column: 'SOME_DROPDOWN',
|
|
selectbox_value: 'Option 3',
|
|
selectbox_order: 3
|
|
}
|
|
)
|
|
|
|
// Write one JSON file per test table with metadata + rows
|
|
for (const [tableName, rows] of Object.entries(testTables)) {
|
|
const tableFile = nodePath.resolve(
|
|
testDataDir,
|
|
tableName.toLowerCase() + '.json'
|
|
)
|
|
const tableData = {
|
|
name: tableName,
|
|
columns: testSchema[tableName] || [],
|
|
rows: rows
|
|
}
|
|
fs.writeFileSync(tableFile, JSON.stringify(tableData, null, 2))
|
|
}
|
|
console.log('TESTDATA data dir: ' + testDataDir)
|
|
|
|
// ─── DC996664 library: extra libref for the multi-load Cypress E2E fixtures ──
|
|
// The multi_load_test_1/2.xlsx fixture files have sheet names like
|
|
// DC996664.MPE_X_TEST, DC996664.MPE_TABLES, DC996664.MPE_VALIDATIONS.
|
|
// Those libref.table combos must exist in MPE_TABLES (so startupservice
|
|
// returns them in sasdatasets and the multi-load sheet matcher accepts
|
|
// them) and have their own data folder (so viewlibs lists the libref).
|
|
const multiLoadDataDir = nodePath.resolve(
|
|
driveRoot,
|
|
'files',
|
|
appLoc,
|
|
'data',
|
|
multiLoadLibref
|
|
)
|
|
if (!fs.existsSync(multiLoadDataDir)) {
|
|
fs.mkdirSync(multiLoadDataDir, { recursive: true })
|
|
}
|
|
tables.MPE_TABLES.push(
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: multiLoadLibref,
|
|
dsn: 'MPE_X_TEST',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'UPDATE',
|
|
buskey: 'PRIMARY_KEY_FIELD',
|
|
notes: 'Multi-load fixture table'
|
|
},
|
|
{
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: multiLoadLibref,
|
|
dsn: 'MPE_TABLES',
|
|
num_of_approvals_required: 1,
|
|
loadtype: 'TXTEMPORAL',
|
|
buskey: 'LIBREF DSN',
|
|
var_txfrom: 'TX_FROM',
|
|
var_txto: 'TX_TO',
|
|
notes: 'Multi-load fixture table'
|
|
}
|
|
)
|
|
console.log(multiLoadLibref + ' data dir: ' + multiLoadDataDir)
|
|
|
|
// Write the control tables back with the fixture rows added. Columns are the
|
|
// ones makedata wrote, or the fixture's own schema where the table is new.
|
|
for (const [tableName, rows] of Object.entries(tables)) {
|
|
saveTable(tableName, rows, schema[tableName])
|
|
}
|
|
|
|
/*
|
|
MPE_X_TEST's column LABELs are a fixture: viewer-labels.cy.ts asserts that
|
|
SOME_CHAR/SOME_DATE show a label that differs from the name while SOME_NUM
|
|
falls back to its name on a blank label. makedata carries SAS's own metadata,
|
|
where the table has no labels at all, so they are added here rather than
|
|
making the control library say something the SAS service does not.
|
|
*/
|
|
const xTestLabels = {
|
|
SOME_CHAR: 'Some Character Column',
|
|
SOME_DATE: 'Some Date',
|
|
SOME_DATETIME: 'Some Datetime',
|
|
SOME_TIME: 'Some Time'
|
|
}
|
|
const xTest = readTable('MPE_X_TEST')
|
|
if (xTest) {
|
|
saveTable(
|
|
'MPE_X_TEST',
|
|
xTest.rows,
|
|
xTest.columns.map(function (column) {
|
|
const label = xTestLabels[column.name.toUpperCase()]
|
|
return label ? Object.assign({}, column, { label: label }) : column
|
|
})
|
|
)
|
|
}
|
|
|
|
// Copy DC_JSLIB table files into DC996664 (same schema/rows) so the
|
|
// multi-load fixtures can fetch getdata for DC996664.MPE_X_TEST etc.
|
|
// Must run after the DC_JSLIB write loop above.
|
|
for (const tableName of ['MPE_X_TEST', 'MPE_TABLES']) {
|
|
const srcFile = nodePath.resolve(dataDir, tableName.toLowerCase() + '.json')
|
|
const dstFile = nodePath.resolve(
|
|
multiLoadDataDir,
|
|
tableName.toLowerCase() + '.json'
|
|
)
|
|
/* A copy that fails is a broken multi-load fixture, not a warning: letting
|
|
it through surfaces later as an unrelated Cypress failure, far from the
|
|
seed that caused it. */
|
|
const data = JSON.parse(
|
|
fs.readFileSync(srcFile, { encoding: 'utf8' }).toString()
|
|
)
|
|
fs.writeFileSync(dstFile, JSON.stringify(data, null, 2))
|
|
}
|
|
|
|
|
|
// fixtures are written after the derived rows, which are keyed on TESTDATA
|
|
console.log('createdemodata: TESTDATA=' + testDataLibref + ' MULTILOAD=' + multiLoadLibref)
|
|
|
|
webout('OPEN')
|
|
webout('OBJ', 'createdemodata', [
|
|
{ STATUS: 'SUCCESS', DCLIB: dcLibref, ADMIN: adminGroup }
|
|
])
|
|
webout('CLOSE')
|