@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
246 lines (218 loc) • 8.72 kB
JavaScript
const cds = require ('../lib/cds.js')
const { regex4 } = require ('./util/cli.js')
const { Minify } = require ('./minify.js')
class Export extends Minify {
options = Export.options // for IntelliSense
static options = { ...super.options,
services: /all/,
from: ['*'],
to: './apis/*',
cleanse: { _excluding: 'keep',
'@restrict': true,
'@requires': true,
'@cds.api.': true,
'@cds.on.': true,
'@UI.': true,
includes: true,
ignored: true,
assocs: true,
texts: true,
views: true,
},
reuse: [ '@sap/cds/common' ],
plugin: false,
texts: false,
data: false,
as: 'csn',
for: 'npm',
force: false,
}
static shortcuts = { ...super.shortcuts,
to:'2',
for:'4'
}
static filename = __filename
parseArgs (conf) {
if (!process.stdout.isTTY) this.options.to = '-'
let [sources,o] = super.parseArgs (conf)
// -----------------------------------------------------------------------
// Java-specific adjustments -> shall this stay for GA?
if (o.for === 'mvn' && !o.to.startsWith('./') && !o.to.startsWith('/'))
o.to = './src/main/resources/cds/' + o.to
// -----------------------------------------------------------------------
if (o.from && o.from != '*') { // variant: cds-export <services> --from <models>
if (o.services == '/all/') o.services = regex4 (sources)
sources = o.from
}
return [ this.sources = o.from = sources, this.options = o ]
}
async requires (models) {
if (models == 'none' || models == '') return {check(){}}
const { definitions: defs, $sources } = await cds.load (models, 'parsed')
const $map = Object.fromEntries ($sources.map ((s,i) => [ local(s), models[i] ]))
return Object.assign (new Set, { check(n) {
return n in defs && this.add ($map[defs[n].$location.file])
}})
}
async cleanse (csn, o = this.options) {
const reused = await this.requires (o.reuse)
const { definitions: defs } = csn = { ...csn, definitions: { ...csn.definitions }}
const { cleanse } = o
// cleanse model...
for (const [each,d] of Object.entries (defs)) {
if (reused.check(each)) delete defs [each]
else if (cleanse.ignored && d['@cds.api.ignore']) delete defs [each]
else if (cleanse.texts && each.endsWith('.texts')) delete defs [each]
else if (d.kind === 'entity') _cleanse (d, cleanse)
}
if (reused.size > 0) csn.requires = [...reused]
// cleanse entities...
function _cleanse (d, cleanse) {
if (cleanse.managed) {
delete d.elements.createdAt
delete d.elements.createdBy
delete d.elements.modifiedAt
delete d.elements.modifiedBy
}
if (cleanse.users) {
delete d.elements.createdBy
delete d.elements.modifiedBy
}
if (cleanse.views) {
delete d.projection
delete d.query
}
if (cleanse.texts) {
delete d.elements.localized
delete d.elements.texts
}
// cleanse elements...
for (const [each,e] of Object.entries(d.elements)) {
if (cleanse.ignored && e['@cds.api.ignore']) return delete d.elements[each]
if (cleanse.texts) delete e.localized
if (cleanse.assocs) delete e.keys // cleanse .keys added by cdsc
}
}
return super.cleanse (csn,o)
}
async output (minified,o) {
if (this.options.to === '-') return super.output (minified)
if (this.options.to.endsWith('/*')) this.options.to = this.export_to()
console.log(`\n${this.name}ing APIs to`, cds.utils.local(this.options.to), '...\n')
const exports = []; exports.push(...this.export_models (minified))
if (o.texts) exports.push (...this.export_texts (minified))
if (o.data) exports.push (this.export_data (minified))
if (o.for === 'npm') exports.push (this.export_package_json())
await Promise.all (exports)
console.log()
}
export_to() {
const to = this.options.to.replace('/*','')
const { resolve, basename } = cds.utils.path
const { sources } = this
if (sources.length === 1 && sources[0].endsWith('.cds'))
return resolve (to, this.package_name = basename(sources[0]).slice(0,-4))
const defs = this.min.definitions
const services = Object.keys(defs).filter (k => defs[k].kind === 'service')
if (services.length === 1)
return resolve (to, this.package_name = services[0])
else throw `\n Can't determine an output folder. \n Please specify one using --to option. \n`
}
async export_package_json (pkg, o = this.options) {
if (!o.force && exists (path.join (this.options.to, 'package.json'))) return null
if (!pkg) {
let { name, version } = await read ('package.json')
name += '-' + cds.utils.path.basename (this.options.to)
pkg = { name, version }
if (o.plugin) {
let requires = Object.fromEntries (Object.entries(this.min.definitions)
.filter (([,d]) => d.kind === 'service')
.map (([name]) => [ name, true ]))
pkg.cds = {requires}
}
}
if (o.plugin) await this.write ('// just a tag file for plug & play') .to ('cds-plugin.js', false)
return this.write (pkg) .to ('package.json')
}
*export_models (model, o = this.options) {
for (let d of Object.values(model.definitions))
if (d.kind === 'service') d['@cds.external'] ??= 2
yield this.write (
`// This file acts as a central facade to exported service definitions.\n`+
`// You can modify it to tweak things, without your changes being overridden.\n`+
`using from './services';`
) .to ('index.cds', false)
if (o.as === 'cdl') {
const cdl = cds.compile.to.cdl (model)
yield this.write (cdl) .to ('services.cds')
} else {
const csn = cds.compile.to.json (model)
yield this.write (csn) .to ('services.csn')
}
}
*export_texts() {
const common = cds.load.properties (require.resolve('@sap/cds/_i18n/i18n.properties'))
const bundles = cds.i18n.labels.all()
const defaults = cds.i18n.labels.defaults
const serialize = texts => Object.entries(texts).map(([k,v]) => k +'='+ v.replace(/'/g, "''")).join('\n')
const write = texts => ({ to: file => this.write(serialize(texts)).to(file) })
// minify fallback bundle entries
const fallback = bundles[''] = {}
for (let key in defaults) {
if (key in common) continue
fallback[key] = defaults[key]
}
yield write(fallback) .to ('_i18n/i18n.properties')
// minify other bundles
for (let [locale,texts] of Object.entries(bundles)) {
const min = bundles[locale] = {}
for (let key in texts) {
if (key in common) continue
if (texts[key] === defaults[key]) continue
min[key] = texts[key]
}
if (Object.keys(min).length > 0) write(min) .to (`_i18n/i18n_${locale}.properties`)
}
}
async export_data (model) {
const db = cds.db = await cds.deploy ('*',{silent:true})
const out = path.resolve (this.options.to, 'data')
await fs.promises.rm (out, { recursive: true, force: true })
await mkdirp (this.options.to, 'data')
for (let each of cds.linked(model).entities) {
await _export (db.model.definitions[each.name])
}
async function _export (e) {
if (!e || e['@cds.persistence.skip']) return
const file = path.join (out, `${e.name}.csv`)
const columns = Object.keys(e.elements) //.filter(k => !e.elements[k]['@Core.Computed'])
const rows = await SELECT.from(e).columns(columns)
let csv = fs.createWriteStream (file), i=0
for (let r of await rows) {
if (i++ === 0) csv.write (Object.keys(r).join(',') +'\n')
csv.write (Object.values(r).map(quoted).join(',') +'\n')
}
csv.end()
console.log (DIMMED,' >', local(file), RESET)
}
function quoted (x) {
if (typeof x === 'string') {
if (x.startsWith('"') && x.endsWith('"')) return x // already quoted
if (x.endsWith(',')) x = x.slice(0, -1) // remove trailing comma
if (x.includes(',') || x.includes('\n')) return `"${x.replace(/"/g, '""')}"`
}
return x
}
}
write (x) { return { to: async (filename, force=true) => {
if (!x) return; else if (x.then) x = await x
const file = path.join (this.options.to, filename)
if (!force && !this.options.force && exists(file)) return
console.log (DIMMED,' >', local(file), RESET)
return write(x).to (file)
}}}
}
const { read, write, exists, mkdirp, local, path, fs } = cds.utils
const { DIMMED, RESET } = cds.utils.colors
module.exports = Object.assign (Export._for_cds_dk(), { Export })