Finding 1 on #510 was that getddl sent an octet-stream, quoted-filename header the estate never sends. The same defect was in every other download path, so this fixes the class rather than the one service. Every value below was read from the synthetic estate's own response, except where noted. public/getrawdata, from getrawdata.sas and its recorded answers: SAS application/text LIBREF.DS.sas CSV application/csv LIBREF.DS.csv EXCEL openxml sheet LIBREF.DS.xlsx (from the service source: PROC EXPORT xlsx fails on the synthetic estate with syscc=1012, so it cannot be recorded there) MARKDOWN text/markdown LIBREF.DS.md PDF application/pdf LIBREF.DS.pdf - the filename is the upcased LIBREF.DS plus the type's extension, unquoted - WEBCSV and WEBTAB send no headers at all: the estate writes those rows straight to _webout, and a content-disposition would advertise a download it does not offer - a type outside that list aborts "Invalid type" with mac=&_program..sas before anything is streamed, which is what the estate answers for FOOBAR admin/exportdb: %mp_streamfile(contenttype=TEXT, outname=&dc_libref..ddl), so application/text and an unquoted <libref>.ddl - the mock sent text/plain. The shared mpStreamfile helper in dcMockUtils (exportconfig, getauditfile) now writes one header per line with the filename unquoted, as %mfs_httpheader does, so the zip downloads stop quoting a name the estate does not quote. Tests: sas/mocks/test/download-headers.test.js (renamed from getddl-headers.test.js) now covers all four paths - 9 tests. Each of the new assertions was run against the old behaviour first and fails there: text/csv instead of application/csv, a quoted filename, and WEBCSV aborting instead of streaming. Whole mock suite: 142 tests, 142 pass.
166 lines
6.7 KiB
JavaScript
166 lines
6.7 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 ─────────────────────
|
|
|
|
/* exportdb.sas streams with %mp_streamfile(contenttype=TEXT
|
|
,outname=&dc_libref..ddl): an application/text attachment named
|
|
<libref>.ddl, unquoted. Confirmed against the estate's own response
|
|
(`content-type: application/text`,
|
|
`content-disposition: attachment; filename=DC634301.ddl`). */
|
|
const filename = `${libref}.ddl`
|
|
fs.writeFile(
|
|
_SASJS_WEBOUT_HEADERS,
|
|
`Content-Type: application/text\nContent-disposition: attachment; filename=${filename}`,
|
|
function (err) {
|
|
if (err) throw err
|
|
}
|
|
)
|
|
|
|
_webout = lines.join('\n') + '\n'
|