Review feedback on this PR. - getdata.js read MPE_CONFIG from the requested table's own library, so the read failed for every non-control libref and both limits silently fell back to the shipped defaults. It now reads mpeDataDir, as viewdata.js already did. Reproduced first: with DC_MAXOBS_WEBEDIT=5, DC_JSLIB.MPE_X_TEST aborted while TESTDATA.DEMO_01 loaded untouched. - exportdb.js mapped DATETIME to TIMESTAMP for TSQL as well, and T-SQL's TIMESTAMP is a deprecated synonym for rowversion, so datetime columns were declared as rowversions. TSQL now gets DATETIME2; PGSQL still gets TIMESTAMP. DOUBLE PRECISION is left alone - it is a T-SQL synonym for float, not an invalid type. - loadDcConfig read the config row as direct properties, but a row that has been through the app's own edit-and-approve flow comes back from the adapter with upper-case keys, so the approved option was skipped and fell back to its default - the same silent-default failure by another route. It now reads every field through colVal, like the rest of the mocks. - row-cell-limits.cy.ts gains a case that moves DC_MAXCELLS_WEBEDIT to a non-default value through the editor, asserts that a non-control-libref table is then refused, and puts the option back. It fails if the read is dropped: verified by reinstating the getdata.js bug and watching it fail.
161 lines
6.4 KiB
JavaScript
161 lines
6.4 KiB
JavaScript
const nodePath = require('path')
|
|
|
|
let appLoc = nodePath.join(..._program.split('services')[0].split('/'))
|
|
const sasjsRoot = nodePath.resolve(weboutPath, '..', '..', '..')
|
|
const driveRoot = nodePath.resolve(sasjsRoot, 'drive')
|
|
|
|
eval(fs.readFileSync(nodePath.resolve(driveRoot, 'files', appLoc, 'services', 'dcMockUtils.js'), 'utf8'))
|
|
|
|
/**
|
|
* Mock of services/admin/exportdb.sas - exports the DC library as DDL.
|
|
*
|
|
* The real service streams mp_ds2ddl output for every table in &DC_LIBREF, in
|
|
* the requested flavour, and mp_lib2inserts appends the rows for the SAS and
|
|
* PGSQL flavours. This mock emits the DDL only - a basic, readable rendering
|
|
* per flavour, enough to exercise the System screen's Export DC Library DDL
|
|
* action end to end (the download headers, the filename, the flavour and the
|
|
* optional schema). It deliberately does not append INSERTs: the real service
|
|
* does, but for a mock that would mean serialising the whole library's rows
|
|
* (MPE_X_WIDE alone is 200,000 cells) for no extra test value.
|
|
*
|
|
* Params arrive from the URL the client opens:
|
|
* <serverUrl>/SASjsApi/stp/execute/?_program=<appLoc>/services/admin/exportdb
|
|
* &flavour=SAS|PGSQL|TSQL[&schema=<name>]
|
|
* The schema box only applies to the DB flavours, and the client omits the
|
|
* param entirely when it is blank, so both must be read defensively - an
|
|
* unset URL param is an undeclared const here, not an empty string.
|
|
*/
|
|
|
|
const ddlFlavour = (typeof flavour === 'undefined' || !flavour)
|
|
? 'SAS'
|
|
: String(flavour).toUpperCase()
|
|
|
|
const dcConf = loadDcSettings(driveRoot, appLoc)
|
|
const libref = dcConf.dcLibref
|
|
const dataDir = dcConf.dataDir
|
|
|
|
// if no schema param, the DC libref is used (as in exportdb.sas)
|
|
const ddlSchema = (typeof schema === 'undefined' || !schema)
|
|
? libref
|
|
: String(schema)
|
|
|
|
// ─── Column types, per flavour ───────────────────────────────────────────────
|
|
|
|
function colType(col, flavour) {
|
|
const isChar = String(col.type).toUpperCase() === 'C'
|
|
const fmt = String(col.format || '').toUpperCase()
|
|
const len = Number(col.length) || (isChar ? 1 : 8)
|
|
|
|
if (flavour === 'SAS') {
|
|
return isChar ? `char(${len})` : 'num'
|
|
}
|
|
|
|
// PGSQL and TSQL share the mapping; only the quoting differs. The one type
|
|
// that must not be shared is the datetime: T-SQL's TIMESTAMP is a deprecated
|
|
// synonym for rowversion - a binary row version, not a datetime - so it maps
|
|
// to DATETIME2 there. DOUBLE PRECISION is a T-SQL synonym for float, so it
|
|
// stays valid on both.
|
|
if (isChar) return `VARCHAR(${len})`
|
|
if (fmt.startsWith('DATETIME'))
|
|
return flavour === 'TSQL' ? 'DATETIME2' : 'TIMESTAMP'
|
|
if (fmt.startsWith('DATE')) return 'DATE'
|
|
if (fmt.startsWith('TIME')) return 'TIME'
|
|
return 'DOUBLE PRECISION'
|
|
}
|
|
|
|
function colRef(name, flavour) {
|
|
if (flavour === 'PGSQL') return `"${name}"`
|
|
if (flavour === 'TSQL') return `[${name}]`
|
|
return name
|
|
}
|
|
|
|
function tableRef(table, flavour) {
|
|
if (flavour === 'TSQL') return `[${ddlSchema}].[${table}]`
|
|
return `${ddlSchema}.${table}`
|
|
}
|
|
|
|
// ─── Buskeys (for the unique index), from MPE_TABLES ─────────────────────────
|
|
|
|
const buskeys = {}
|
|
try {
|
|
const reg = makeTableLoader(dataDir)('MPE_TABLES')
|
|
for (const row of ((reg && reg.rows) || [])) {
|
|
const dsn = String(colVal(row, 'DSN') || '').trim().toUpperCase()
|
|
const buskey = String(colVal(row, 'BUSKEY') || '').trim()
|
|
if (dsn && buskey) buskeys[dsn] = buskey
|
|
}
|
|
} catch(err) { /* no MPE_TABLES - the DDL is still emitted, without indexes */ }
|
|
|
|
// ─── Build the DDL ───────────────────────────────────────────────────────────
|
|
|
|
const lines = []
|
|
const stamp = new Date().toISOString().replace('T', ':').substring(0, 19)
|
|
lines.push(`/* DDL generated by the Data Controller mock on ${stamp} */`)
|
|
|
|
if (ddlFlavour === 'PGSQL') {
|
|
lines.push(`CREATE SCHEMA ${ddlSchema};`)
|
|
}
|
|
|
|
let tables = []
|
|
try {
|
|
tables = fs.readdirSync(dataDir)
|
|
.filter(f => f.endsWith('.json'))
|
|
.map(f => {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(nodePath.resolve(dataDir, f), {encoding:'utf8'}).toString())
|
|
} catch(e) { return null }
|
|
})
|
|
.filter(t => t && t.name && Array.isArray(t.columns))
|
|
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
} catch(err) { /* nothing to export */ }
|
|
|
|
for (const table of tables) {
|
|
const tableName = String(table.name).toUpperCase()
|
|
const cols = table.columns.filter(c => String(c.name || '').trim() !== '')
|
|
if (cols.length === 0) continue
|
|
|
|
if (ddlFlavour === 'SAS') {
|
|
lines.push(`/* SAS flavour DDL for ${libref}.${tableName} */`)
|
|
lines.push(`create table ${tableRef(tableName, ddlFlavour)}(`)
|
|
} else if (ddlFlavour === 'PGSQL') {
|
|
lines.push(`/* Postgres Flavour DDL for ${libref}.${tableName} */`)
|
|
lines.push(`CREATE TABLE ${tableRef(tableName, ddlFlavour)} (`)
|
|
} else {
|
|
lines.push(`/* TSQL Flavour DDL for ${libref}.${tableName} */`)
|
|
lines.push(`create table ${tableRef(tableName, ddlFlavour)}(`)
|
|
}
|
|
|
|
cols.forEach((col, idx) => {
|
|
const name = colRef(String(col.name).toUpperCase(), ddlFlavour)
|
|
const prefix = idx === 0 ? ' ' : ' ,'
|
|
lines.push(`${prefix}${name} ${colType(col, ddlFlavour)}`)
|
|
})
|
|
lines.push(');')
|
|
|
|
// a unique index on the buskey, where MPE_TABLES declares one - SAS has no
|
|
// equivalent in mp_ds2ddl output, so it is skipped for that flavour
|
|
const buskey = buskeys[tableName]
|
|
if (buskey && ddlFlavour !== 'SAS') {
|
|
const keys = buskey.split(/[\s,]+/).filter(Boolean)
|
|
.map(k => colRef(k.toUpperCase(), ddlFlavour))
|
|
lines.push(
|
|
`CREATE UNIQUE INDEX ${colRef(tableName + '_pk', ddlFlavour)} ON ` +
|
|
`${tableRef(tableName, ddlFlavour)}(\n ${keys.join('\n ,')}\n);`
|
|
)
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
// ─── Stream it back as a download, as mp_streamfile does ─────────────────────
|
|
|
|
const filename = `${libref}.ddl`
|
|
fs.writeFile(
|
|
_SASJS_WEBOUT_HEADERS,
|
|
`Content-type: text/plain \n Content-Disposition: attachment;filename="${filename}"`,
|
|
function (err) {
|
|
if (err) throw err
|
|
}
|
|
)
|
|
|
|
_webout = lines.join('\n') + '\n'
|